Thermodynamic Phase Diagram (Pressure-Temperature) — plotnine

A pressure-temperature (P-T) phase diagram showing the boundaries between solid, liquid, and gas phases of a substance. The diagram includes the triple point where all three phases coexist and the critical point beyond which the liquid-gas distinction vanishes. This is one of the most fundamental diagrams in chemistry and physics, essential for understanding phase transitions and states of matter.

Thermodynamic Phase Diagram (Pressure-Temperature) rendered with plotnine

Python source (plotnine)

""" anyplot.ai
phase-diagram-pt: Thermodynamic Phase Diagram (Pressure-Temperature)
Library: plotnine 0.15.5 | Python 3.13.13
Quality: 90/100 | Updated: 2026-06-08
"""

import os
import sys

import numpy as np
import pandas as pd


# Work around naming conflict: plotnine.py script shadows the plotnine package
_script_dir = os.path.dirname(os.path.abspath(__file__))
if _script_dir in sys.path:
    sys.path.remove(_script_dir)
if "" in sys.path:
    sys.path.remove("")
if "." in sys.path:
    sys.path.remove(".")

from plotnine import (
    aes,
    annotate,
    element_blank,
    element_line,
    element_rect,
    element_text,
    geom_line,
    geom_point,
    geom_ribbon,
    geom_vline,
    ggplot,
    labs,
    scale_color_manual,
    scale_x_continuous,
    scale_y_log10,
    theme,
    theme_minimal,
)


# Theme tokens — Imprint palette, theme-adaptive chrome
THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Imprint categorical palette (hybrid-v3 sort order)
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]
ANYPLOT_AMBER = "#DDCC77"  # warning / caution anchor — used for special point markers

# Data — Water phase diagram (accurate physical constants)
# Triple point: 273.16 K, 611.73 Pa
# Critical point: 647.1 K, 22.064 MPa
R = 8.314  # J/(mol·K)
T_triple = 273.16
P_triple = 611.73
T_critical = 647.1
P_critical = 22.064e6

# Solid-gas boundary (sublimation curve) via Clausius-Clapeyron
L_sub = 51060  # J/mol, latent heat of sublimation
T_sub = np.linspace(200, T_triple, 80)
P_sub = P_triple * np.exp((L_sub / R) * (1 / T_triple - 1 / T_sub))

# Liquid-gas boundary (vaporization curve) via Clausius-Clapeyron
L_vap = 40700  # J/mol, latent heat of vaporization
T_vap = np.linspace(T_triple, T_critical, 100)
P_vap = P_triple * np.exp((L_vap / R) * (1 / T_triple - 1 / T_vap))

# Solid-liquid boundary (melting curve) — water's anomalous negative slope
P_melt = np.logspace(np.log10(P_triple), np.log10(P_critical * 5), 80)
T_melt = T_triple - 7.4e-8 * (P_melt - P_triple)

# Build dataframe for boundary curves
df_sub = pd.DataFrame({"temperature": T_sub, "pressure": P_sub, "boundary": "Solid–Gas"})
df_vap = pd.DataFrame({"temperature": T_vap, "pressure": P_vap, "boundary": "Liquid–Gas"})
df_melt = pd.DataFrame({"temperature": T_melt, "pressure": P_melt, "boundary": "Solid–Liquid"})
df = pd.concat([df_sub, df_vap, df_melt], ignore_index=True)

# Special points (triple + critical)
df_points = pd.DataFrame({"temperature": [T_triple, T_critical], "pressure": [P_triple, P_critical]})

# Phase region fills — separate ribbons per region (plotnine layer composition)
P_min = 10
P_max = 2e9
T_min = 180
T_max = 750

n_fill = 60
T_solid_fill = np.linspace(T_min, T_triple - 1, n_fill)
P_solid_lower = P_triple * np.exp((L_sub / R) * (1 / T_triple - 1 / T_solid_fill))
df_solid = pd.DataFrame({"temperature": T_solid_fill, "ymin": P_solid_lower, "ymax": np.full(n_fill, P_max)})

T_gas_sub = np.linspace(T_min, T_triple, 30)
P_gas_sub_upper = P_triple * np.exp((L_sub / R) * (1 / T_triple - 1 / T_gas_sub))
T_gas_vap = np.linspace(T_triple, T_critical, 30)
P_gas_vap_upper = P_triple * np.exp((L_vap / R) * (1 / T_triple - 1 / T_gas_vap))
T_gas_beyond = np.linspace(T_critical, T_max, 15)
P_gas_beyond_upper = np.full(15, P_critical)
df_gas = pd.DataFrame(
    {
        "temperature": np.concatenate([T_gas_sub, T_gas_vap, T_gas_beyond]),
        "ymin": np.full(75, P_min),
        "ymax": np.concatenate([P_gas_sub_upper, P_gas_vap_upper, P_gas_beyond_upper]),
    }
)

T_liq = np.linspace(T_triple, T_critical, n_fill)
P_liq_lower = P_triple * np.exp((L_vap / R) * (1 / T_triple - 1 / T_liq))
df_liquid = pd.DataFrame({"temperature": T_liq, "ymin": P_liq_lower, "ymax": np.full(n_fill, P_max)})

T_sc = np.linspace(T_critical, T_max, 30)
df_supercritical = pd.DataFrame({"temperature": T_sc, "ymin": np.full(30, P_critical), "ymax": np.full(30, P_max)})

# Imprint palette: boundaries in canonical order (positions 1–3)
boundary_colors = {
    "Solid–Gas": IMPRINT_PALETTE[0],  # brand green
    "Liquid–Gas": IMPRINT_PALETTE[1],  # lavender
    "Solid–Liquid": IMPRINT_PALETTE[2],  # blue
}

# Pressure axis: pre-computed breaks and labels spanning 9 log-scale decades
pressure_breaks = [10, 100, 1000, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9]
pressure_labels = ["10 Pa", "100 Pa", "1 kPa", "10 kPa", "100 kPa", "1 MPa", "10 MPa", "100 MPa", "1 GPa"]

title = "phase-diagram-pt · python · plotnine · anyplot.ai"
n = len(title)
title_fontsize = round(12 * 67 / n) if n > 67 else 12

plot = (
    ggplot()
    # Phase region fills — color matches corresponding boundary curve
    + geom_ribbon(
        data=df_solid,
        mapping=aes(x="temperature", ymin="ymin", ymax="ymax"),
        fill=IMPRINT_PALETTE[2],
        alpha=0.15,
        inherit_aes=False,
    )
    + geom_ribbon(
        data=df_gas,
        mapping=aes(x="temperature", ymin="ymin", ymax="ymax"),
        fill=IMPRINT_PALETTE[0],
        alpha=0.15,
        inherit_aes=False,
    )
    + geom_ribbon(
        data=df_liquid,
        mapping=aes(x="temperature", ymin="ymin", ymax="ymax"),
        fill=IMPRINT_PALETTE[1],
        alpha=0.15,
        inherit_aes=False,
    )
    + geom_ribbon(
        data=df_supercritical,
        mapping=aes(x="temperature", ymin="ymin", ymax="ymax"),
        fill=IMPRINT_PALETTE[3],
        alpha=0.15,
        inherit_aes=False,
    )
    # Boundary curves — Imprint palette canonical order
    + geom_line(data=df, mapping=aes(x="temperature", y="pressure", color="boundary"), size=1.5)
    # Triple point and critical point: amber fill, ink border
    + geom_point(
        data=df_points,
        mapping=aes(x="temperature", y="pressure"),
        color=INK,
        fill=ANYPLOT_AMBER,
        size=4.5,
        stroke=1.2,
        shape="o",
        inherit_aes=False,
    )
    + scale_color_manual(values=boundary_colors)
    + scale_y_log10(breaks=pressure_breaks, labels=pressure_labels, limits=(P_min, P_max))
    + scale_x_continuous(limits=(T_min, T_max))
    # Phase region labels — color matches fill color for visual coherence
    + annotate("text", x=218, y=5e7, label="SOLID", size=3.5, color=IMPRINT_PALETTE[2], alpha=0.75, fontweight="bold")
    + annotate("text", x=460, y=5e7, label="LIQUID", size=3.5, color=IMPRINT_PALETTE[1], alpha=0.75, fontweight="bold")
    + annotate("text", x=490, y=50, label="GAS", size=3.5, color=IMPRINT_PALETTE[0], alpha=0.75, fontweight="bold")
    + annotate(
        "text",
        x=698,
        y=2e8,
        label="SUPERCRITICAL",
        size=2.8,
        color=IMPRINT_PALETTE[3],
        alpha=0.75,
        fontweight="bold",
        ha="center",
    )
    # Triple point coordinate annotation
    + annotate(
        "text",
        x=T_triple + 18,
        y=P_triple * 0.25,
        label=f"Triple Point\n({T_triple} K, {P_triple:.0f} Pa)",
        size=2.8,
        color=INK_SOFT,
        ha="left",
        va="top",
    )
    # Critical point coordinate annotation
    + annotate(
        "text",
        x=T_critical - 15,
        y=P_critical * 0.15,
        label=f"Critical Point\n({T_critical} K, {P_critical / 1e6:.1f} MPa)",
        size=2.8,
        color=INK_SOFT,
        ha="right",
        va="top",
    )
    # Dashed vertical line at critical temperature
    + geom_vline(xintercept=T_critical, linetype="dashed", color=INK_MUTED, size=0.4, alpha=0.5)
    + labs(x="Temperature (K)", y="Pressure", title=title, color="Phase Boundary")
    + theme_minimal()
    + theme(
        figure_size=(8, 4.5),
        text=element_text(size=7, color=INK_SOFT),
        axis_title=element_text(size=10, color=INK),
        axis_text=element_text(size=8, color=INK_SOFT),
        plot_title=element_text(size=title_fontsize, color=INK),
        legend_title=element_text(size=8, color=INK),
        legend_text=element_text(size=8, color=INK_SOFT),
        legend_position="right",
        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT),
        panel_background=element_rect(fill=PAGE_BG),
        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),
        panel_grid_major=element_line(color=INK, size=0.3, alpha=0.15),
        panel_grid_minor=element_blank(),
        axis_line_x=element_line(color=INK_SOFT, size=0.5),
        axis_line_y=element_line(color=INK_SOFT, size=0.5),
        panel_border=element_rect(color=INK_SOFT, fill=None),
    )
)

plot.save(f"plot-{THEME}.png", dpi=400, width=8, height=4.5, units="in", verbose=False)

Part of Thermodynamic Phase Diagram (Pressure-Temperature) on anyplot.ai.

Other implementations