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_yzoom into a region — absolute humidity [kg/kg] and the y-coordinate (≈ temperature at x = 0).altitudesets the site altitude (m a.s.l., shown top-right) and the air pressure derived from it; passpressure(Pa) instead if you know the actual pressure.show_temperature,show_density,show_rel_humidity,show_enthalpy,show_abs_humiditytoggle each iso-line family individually.conventionswitches the coordinate normalization between'classical'(default, Recknagel) and'glueck'.heightsets the diagram height in pixels.captionsandtextsreplace 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,
)