Thermodynamic Phase Diagram (Pressure-Temperature) — Pygal

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 Pygal

Python source (Pygal)

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

# Ensure we import the installed pygal package, not this file
import importlib.util
import os
import sys
import xml.etree.ElementTree as ET

import cairosvg
import numpy as np


pygal_spec = importlib.util.find_spec("pygal")
if pygal_spec and pygal_spec.origin != __file__:
    import pygal
    from pygal.style import Style
else:
    _cwd = os.getcwd()
    sys.path = [p for p in sys.path if os.path.abspath(p) != _cwd]
    try:
        import pygal
        from pygal.style import Style
    finally:
        sys.path.insert(0, _cwd)

THEME = os.getenv("ANYPLOT_THEME", "light")
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"

# Imprint palette — theme-independent categorical colors
IMPRINT_PALETTE = ("#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314")

# Water phase diagram (physically accurate constants)
triple_t = 273.16  # K
triple_p = 611.73  # Pa
critical_t = 647.1  # K
critical_p = 22.064e6  # Pa
R = 8.314  # J/(mol·K)

# Sublimation curve (Solid-Gas): from 200 K to triple point
sublimation_temps = np.linspace(200, triple_t, 80)
L_sub = 51059  # J/mol — sublimation enthalpy of water
sublimation_pressures = triple_p * np.exp((L_sub / R) * (1 / triple_t - 1 / sublimation_temps))

# Vaporization curve (Liquid-Gas): from triple point to critical point
vaporization_temps = np.linspace(triple_t, critical_t, 100)
L_vap = 40700  # J/mol — vaporization enthalpy of water
vaporization_pressures = triple_p * np.exp((L_vap / R) * (1 / triple_t - 1 / vaporization_temps))

# Melting curve (Solid-Liquid): water has anomalous negative slope
melting_pressures = np.logspace(np.log10(triple_p), np.log10(critical_p * 5), 80)
melting_temps = triple_t - (melting_pressures - triple_p) * 7.5e-8


# Pressure formatter — handles scalar and (x, y) tuple passed by pygal's XY formatter
def fmt_p(v):
    p = v[1] if isinstance(v, (list, tuple)) else v
    return f"{p / 1e6:.1f} MPa" if p >= 1e6 else (f"{p / 1e3:.1f} kPa" if p >= 1e3 else f"{p:.1f} Pa")


# Title: 46 chars < 67 baseline → no scaling needed, use default 66
title = "phase-diagram-pt · python · pygal · anyplot.ai"

custom_style = Style(
    background=PAGE_BG,
    plot_background=PAGE_BG,
    foreground=INK,
    foreground_strong=INK,
    foreground_subtle=INK_MUTED,
    colors=IMPRINT_PALETTE,
    font_family="DejaVu Sans, Helvetica, Arial, sans-serif",
    title_font_family="DejaVu Sans, Helvetica, Arial, sans-serif",
    title_font_size=66,
    label_font_size=56,
    major_label_font_size=44,
    value_font_size=36,
    legend_font_size=44,
    legend_font_family="DejaVu Sans, Helvetica, Arial, sans-serif",
    label_font_family="DejaVu Sans, Helvetica, Arial, sans-serif",
    major_label_font_family="DejaVu Sans, Helvetica, Arial, sans-serif",
    value_font_family="DejaVu Sans, Helvetica, Arial, sans-serif",
    stroke_width=5,
    opacity=0.92,
    opacity_hover=1.0,
    guide_stroke_color=INK_MUTED,
    guide_stroke_dasharray="6,6",
    tooltip_font_size=36,
    tooltip_font_family="DejaVu Sans, Helvetica, Arial, sans-serif",
    tooltip_border_radius=8,
)

chart = pygal.XY(
    width=3200,
    height=1800,
    style=custom_style,
    title=title,
    x_title="Temperature (K)",
    y_title="Pressure (Pa)",
    show_legend=True,
    legend_at_bottom=True,
    legend_box_size=22,
    dots_size=2,
    stroke=True,
    show_x_guides=True,
    show_y_guides=True,
    logarithmic=True,
    explicit_size=True,
    truncate_legend=-1,
    spacing=20,
    margin=40,
    margin_bottom=120,
    margin_left=220,
    margin_right=160,
    tooltip_fancy_mode=True,
    tooltip_border_radius=8,
    interpolate="cubic",
    interpolation_precision=200,
    xrange=(180, 720),
    print_values=False,
    human_readable=True,
    x_value_formatter=lambda x: f"{x:.0f} K",
    value_formatter=fmt_p,
    y_labels=[1, 100, 1e4, 1e6, 1e8],
    y_label_rotation=0,
)

# Series 1–3: phase boundary curves — Imprint positions 1 (green), 2 (lavender), 3 (blue)
sublimation_points = [
    {"value": (float(t), float(p)), "label": f"Sublimation: {t:.0f} K, {fmt_p(p)}"}
    for t, p in zip(sublimation_temps[::4], sublimation_pressures[::4], strict=True)
]
chart.add(
    "Solid ↔ Gas (Sublimation)",
    sublimation_points,
    show_dots=False,
    stroke_style={"width": 8, "linecap": "round", "linejoin": "round"},
    formatter=fmt_p,
)

vaporization_points = [
    {"value": (float(t), float(p)), "label": f"Vaporization: {t:.0f} K, {fmt_p(p)}"}
    for t, p in zip(vaporization_temps[::5], vaporization_pressures[::5], strict=True)
]
chart.add(
    "Liquid ↔ Gas (Vaporization)",
    vaporization_points,
    show_dots=False,
    stroke_style={"width": 8, "linecap": "round", "linejoin": "round"},
    formatter=fmt_p,
)

melting_points = [
    {"value": (float(t), float(p)), "label": f"Melting: {t:.2f} K, {fmt_p(p)}"}
    for t, p in zip(melting_temps[::4], melting_pressures[::4], strict=True)
]
chart.add(
    "Solid ↔ Liquid (Melting)",
    melting_points,
    show_dots=False,
    stroke_style={"width": 8, "linecap": "round", "linejoin": "round"},
    formatter=fmt_p,
)

# Landmark points — series order controls legend swatch via palette cycling.
# Series 4 → palette[3] = ochre #BD8233 → Critical Point (no per-point override needed)
# Series 5 → palette[4] = matte red #AE3030 → Triple Point (no per-point override needed)
chart.add(
    f"Critical Point ({critical_t} K, {fmt_p(critical_p)})",
    [
        {
            "value": (float(critical_t), float(critical_p)),
            "label": f"Critical Point — liquid-gas distinction vanishes\n{critical_t} K, {fmt_p(critical_p)}",
        }
    ],
    dots_size=18,
    stroke=False,
    formatter=fmt_p,
)

chart.add(
    f"Triple Point ({triple_t} K, {triple_p:.0f} Pa)",
    [
        {
            "value": (float(triple_t), float(triple_p)),
            "label": f"Triple Point — all three phases coexist\n{triple_t} K, {triple_p:.0f} Pa",
        }
    ],
    dots_size=18,
    stroke=False,
    formatter=fmt_p,
)

# SVG post-processing: frame removal, phase region labels, callout annotations
svg_string = chart.render(is_unicode=True)
root = ET.fromstring(svg_string)
ns = "http://www.w3.org/2000/svg"

# Remove default chart frame border via CSS override
style_elems = root.findall(f".//{{{ns}}}style")
frame_css = "\nrect.background { stroke: none !important; stroke-width: 0 !important; }"
if style_elems:
    style_elems[0].text = (style_elems[0].text or "") + frame_css

# Phase region labels — semi-transparent, color-coded by boundary membership
phase_labels = [
    {"text": "SOLID", "x": "600", "y": "580", "size": "52", "color": IMPRINT_PALETTE[2], "opacity": "0.35"},
    {"text": "LIQUID", "x": "1370", "y": "400", "size": "52", "color": IMPRINT_PALETTE[1], "opacity": "0.35"},
    {"text": "GAS", "x": "1890", "y": "1050", "size": "52", "color": IMPRINT_PALETTE[0], "opacity": "0.35"},
    {"text": "SUPERCRITICAL", "x": "2730", "y": "155", "size": "36", "color": IMPRINT_PALETTE[3], "opacity": "0.30"},
    {"text": "FLUID", "x": "2730", "y": "193", "size": "36", "color": IMPRINT_PALETTE[3], "opacity": "0.30"},
]

for lbl in phase_labels:
    el = ET.SubElement(root, f"{{{ns}}}text")
    el.set("x", lbl["x"])
    el.set("y", lbl["y"])
    el.set(
        "style",
        f"font-family:DejaVu Sans,Helvetica,Arial,sans-serif;"
        f"font-size:{lbl['size']}px;fill:{lbl['color']};font-weight:700;"
        f"letter-spacing:4px;opacity:{lbl['opacity']};text-anchor:middle;",
    )
    el.text = lbl["text"]

# Callout annotations for thermodynamic landmarks
# Triple point: approx SVG (760, 1040) — callout to upper-right
# Critical point: approx SVG (2640, 250) — callout to lower-left
callouts = [
    # (x_dot, y_dot, x_end, y_end, x_txt, y_txt1, y_txt2, label, sublabel, anchor)
    (760, 1040, 860, 960, 875, 954, 994, "Triple Point", "273.16 K  ·  611.73 Pa", "start"),
    (2640, 250, 2510, 320, 2495, 314, 354, "Critical Point", "647.1 K  ·  22.064 MPa", "end"),
]
for x_d, y_d, x2, y2, x_t, y_t1, y_t2, label, sublabel, anchor in callouts:
    g = ET.SubElement(root, f"{{{ns}}}g")
    ln = ET.SubElement(g, f"{{{ns}}}line")
    ln.set("x1", str(x_d))
    ln.set("y1", str(y_d))
    ln.set("x2", str(x2))
    ln.set("y2", str(y2))
    ln.set("style", f"stroke:{INK_MUTED};stroke-width:2;opacity:0.7;")
    t1 = ET.SubElement(g, f"{{{ns}}}text")
    t1.set("x", str(x_t))
    t1.set("y", str(y_t1))
    t1.set(
        "style",
        f"font-family:DejaVu Sans,Helvetica,Arial,sans-serif;"
        f"font-size:36px;fill:{INK};font-weight:600;text-anchor:{anchor};",
    )
    t1.text = label
    t2 = ET.SubElement(g, f"{{{ns}}}text")
    t2.set("x", str(x_t))
    t2.set("y", str(y_t2))
    t2.set(
        "style",
        f"font-family:DejaVu Sans,Helvetica,Arial,sans-serif;"
        f"font-size:30px;fill:{INK_MUTED};font-weight:400;text-anchor:{anchor};",
    )
    t2.text = sublabel

modified_svg = ET.tostring(root, encoding="unicode")

cairosvg.svg2png(
    bytestring=modified_svg.encode("utf-8"), write_to=f"plot-{THEME}.png", output_width=3200, output_height=1800
)

chart.render_to_file(f"plot-{THEME}.html")

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

Other implementations