Mollier h-x Diagram#

Goal

Display measured indoor conditions on a Mollier h-x (psychrometric) diagram — the relationship between temperature, humidity and enthalpy of moist air, with a comfort zone, dashed warning bands, the outside air with its ventilation line and frequency contour lines.

Data Basis

We use the bundled dataset room_climate_sample.csv shipped with pyedautils — anonymized hourly temperature and humidity from a real sensor in a cool, humidity-controlled room. It is loaded directly from the installed package (importlib.resources), so no download is needed.

Reading the diagram

New to the h-x diagram? The pyedautils guide The h,x diagram in five minutes explains how to read it, the dew point, the comfort zone and how to save energy.

Data Preparation#

import pandas as pd
from importlib import resources

data_path = resources.files("pyedautils") / "data" / "room_climate_sample.csv"
df = pd.read_csv(data_path)   # columns: timestamp, humidity, temperature
df.head()
timestamp humidity temperature
0 2025-05-01 00:00:00 51.0 19.1
1 2025-05-01 01:00:00 51.0 19.1
2 2025-05-01 02:00:00 51.0 19.1
3 2025-05-01 03:00:00 51.0 19.1
4 2025-05-01 04:00:00 50.2 19.1

Visualization#

Pass the DataFrame and the room’s target band to plot_mollier_hx. This cool, humidity-controlled room is kept at 8–18 °C and 40–70 % — the same band as in the Comfort Donuts and Comfort Compass recipes — drawn as the green comfort zone. Measured points are colored by season. All charts are interactive — hover a point for details.

Every diagram below is drawn for the building’s site altitude (here 450 m a.s.l.): plot_mollier_hx derives the air pressure from it, so the psychrometric relations are correct for the thinner air. Altitude and pressure are written in the top-right corner.

from pyedautils.plots import plot_mollier_hx
from IPython.display import HTML, display

altitude = 450  # m a.s.l., the air pressure follows from it; reused below

# Target band of this room: 8-18 °C, 40-70 % rH, no limit on the
# absolute humidity (as in the Comfort Compass recipe)
zone = {"temperature": (8, 18), "rel_humidity": (0.40, 0.70),
        "abs_humidity": None}

html = plot_mollier_hx(df, altitude=altitude, comfort_zone=zone, height=700)
display(HTML(html))

Comfort zone & warning bands#

Two dashed warning bands around the green comfort zone show how far a point is off: comfort_zone_orange and comfort_zone_red, each given as ± offsets around the comfort zone (temperature in K, rel_humidity as a fraction, abs_humidity in kg/kg). comfort_warning_bands() derives these offsets from the severity stages of the Comfort Compass: with the same comfort band, a point inside orange is mild in the compass, inside red moderate, beyond red severe.

An upper limit on the absolute humidity, e.g. "abs_humidity": (0, 0.0115) for 11.5 g/kg, closes the zones on the right: air above it is flagged even when its relative humidity still looks fine (in the compass: hum_abs_band). This room has no such limit.

from pyedautils.comfort import comfort_warning_bands

# Mild / moderate stage limits in K and %-points (the compass defaults)
bands = comfort_warning_bands(temp_stage_limits=(1.0, 2.5),
                              hum_stage_limits=(5.0, 10.0))

html = plot_mollier_hx(
    df,
    altitude=altitude,
    comfort_zone=zone,
    comfort_zone_orange={**bands["orange"], "label": "± 1 K / 5 %"},
    comfort_zone_red={**bands["red"], "label": "± 2.5 K / 10 %"},
    height=700,
)
display(HTML(html))

Outside air & ventilation#

Does airing make the room drier? Pass the outside air as outdoor=(°C, % rH): it is drawn as a blue circle, joined to the latest room reading by a dashed ventilation line along which the room air moves when you air it. Only the absolute humidity decides: this cold, damp night air (2 °C, 85 %) holds less water than the room air, so airing dries the room. In a cool cellar on a muggy summer day it is the other way round (examples in the guide).

from pyedautils.comfort import ventilation_effect
from pyedautils.psychrometrics import pressure_from_altitude

outside = (2, 85)                                   # deg C, % rH
room = tuple(df.iloc[-1][["temperature", "humidity"]])
pressure = pressure_from_altitude(altitude)         # 960 hPa at 450 m
print("Airing", ventilation_effect(room, outside, pressure=pressure))

html = plot_mollier_hx(df, altitude=altitude, outdoor=outside,
                       comfort_zone=zone, height=700)
display(HTML(html))
Airing dries

Frequency contour lines#

With enough data, show_frequency=True overlays cumulative-frequency contour lines: each line labeled N encloses all but N units per year, so the outer lines mark the rare extremes and the inner lines the typical core. Use frequency_unit="days" to count distinct calendar days instead of hours.

Here we switch to a full outdoor weather reference year for Zürich-Kloten (bundled with pyedautils) — the annual spread from cold, dry winters to warm, humid summers is exactly what the contours are meant to show. The comfort zone is hidden (comfort_zone=False), as outdoor weather is not about indoor comfort.

df_kloten = pd.read_csv(
    resources.files("pyedautils") / "data" / "kloten_weather_sample.csv")

html = plot_mollier_hx(df_kloten, altitude=altitude,
                       comfort_zone=False,
                       show_frequency=True, frequency_unit="hours", height=700)
display(HTML(html))

Customization#

Axis ranges, altitude, curve families, texts & convention
  • domain_x / domain_y zoom into a region — absolute humidity [kg/kg] and the y-coordinate (≈ temperature at x = 0).

  • altitude sets the site altitude (m a.s.l., shown top-right) and the air pressure derived from it; pass pressure (Pa) instead if you know the actual pressure.

  • show_temperature, show_density, show_rel_humidity, show_enthalpy, show_abs_humidity toggle each iso-line family individually.

  • convention switches the coordinate normalization between 'classical' (default, Recknagel) and 'glueck'.

  • height sets the diagram height in pixels.

  • captions and texts replace any visible text, e.g. to translate the chart.

html = plot_mollier_hx(
    df,
    altitude=1500,             # site altitude, sets the air pressure
    domain_x=(0.002, 0.014),   # abs. humidity 2–14 g/kg
    domain_y=(10, 35),         # y-range ≈ 10–35 °C
    show_density=False,
    convention="classical",
    height=700,
)