Thermodynamic Phase Diagram (Pressure-Temperature) — Bokeh

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 Bokeh

Python source (Bokeh)

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

import io
import os
import sys


# Prevent self-import: this file is named bokeh.py, so Python adds its directory
# to sys.path[0] and finds this script instead of the installed bokeh package.
_here = os.path.dirname(os.path.abspath(__file__))
sys.path = [p for p in sys.path if not (p and os.path.abspath(p) == _here)]
del _here

import time
from pathlib import Path

import numpy as np
from bokeh.io import output_file, save
from bokeh.models import Arrow, ColumnDataSource, HoverTool, Label, NormalHead, Span
from bokeh.plotting import figure
from PIL import Image
from selenium import webdriver
from selenium.webdriver.chrome.options import Options


# 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 — 8 hues, theme-independent
IMPRINT_PALETTE = ["#009E73", "#C475FD", "#4467A3", "#BD8233", "#AE3030", "#2ABCCD", "#954477", "#99B314"]

# Data — Water phase diagram (realistic Clausius-Clapeyron curves)
# Triple point: 273.16 K, 611.73 Pa | Critical point: 647.1 K, 22.064 MPa
triple_T = 273.16
triple_P = 611.73
critical_T = 647.1
critical_P = 22.064e6
R = 8.314

# Sublimation curve (solid-gas boundary, 200 K → triple point)
T_solid_gas = np.linspace(200, triple_T, 100)
L_sub = 51059  # J/mol, sublimation enthalpy of water
P_solid_gas = triple_P * np.exp((L_sub / R) * (1 / triple_T - 1 / T_solid_gas))

# Vaporization curve (liquid-gas boundary, triple point → critical point)
T_liquid_gas = np.linspace(triple_T, critical_T, 150)
L_vap = 40700  # J/mol, vaporization enthalpy of water
P_liquid_gas = triple_P * np.exp((L_vap / R) * (1 / triple_T - 1 / T_liquid_gas))

# Melting curve (solid-liquid boundary, negative slope — water anomaly)
P_solid_liquid = np.logspace(np.log10(triple_P), np.log10(critical_P * 5), 100)
delta_P = np.maximum(P_solid_liquid - triple_P, 0)
T_solid_liquid = triple_T - delta_P * 7.4e-8 + np.power(delta_P / 1e9, 1.5) * 5

# Title — len=46 < 67 baseline, no shrinking needed
title = "phase-diagram-pt · python · bokeh · anyplot.ai"

# Plot
p = figure(
    width=3200,
    height=1800,
    title=title,
    x_axis_label="Temperature (K)",
    y_axis_label="Pressure (Pa)",
    y_axis_type="log",
    toolbar_location=None,
    tools="",
    x_range=(180, 800),
    y_range=(50, 5e8),
    min_border_bottom=160,
    min_border_left=180,
    min_border_top=110,
    min_border_right=50,
)

# Phase region fills — Imprint palette with semantic color cues
# Ice → cyan (#2ABCCD), water → blue (#4467A3), steam → ochre (#BD8233), supercritical → purple (#C475FD)
solid_T = [180, 180] + T_solid_gas.tolist() + T_solid_liquid.tolist()[::-1]
solid_P = [5e8, 100] + P_solid_gas.tolist() + P_solid_liquid.tolist()[::-1]
p.patch(solid_T, solid_P, fill_color="#2ABCCD", fill_alpha=0.18, line_color=None)

gas_T = T_solid_gas.tolist() + [triple_T] + T_liquid_gas.tolist() + [800, 800, 180]
gas_P = P_solid_gas.tolist() + [triple_P] + P_liquid_gas.tolist() + [critical_P, 100, 100]
p.patch(gas_T, gas_P, fill_color="#BD8233", fill_alpha=0.18, line_color=None)

liquid_T = [triple_T] + T_liquid_gas.tolist() + [critical_T] + T_solid_liquid.tolist()[::-1]
liquid_P = [triple_P] + P_liquid_gas.tolist() + [5e8] + P_solid_liquid.tolist()[::-1]
p.patch(liquid_T, liquid_P, fill_color="#4467A3", fill_alpha=0.18, line_color=None)

sc_T = [critical_T, 800, 800, critical_T]
sc_P = [critical_P, critical_P, 5e8, 5e8]
p.patch(sc_T, sc_P, fill_color="#C475FD", fill_alpha=0.15, line_color=None)

# Phase boundary lines — INK_SOFT (structural chrome, same weight for all boundaries)
p.line(T_solid_gas, P_solid_gas, line_width=4, line_color=INK_SOFT, line_alpha=0.85)
p.line(T_liquid_gas, P_liquid_gas, line_width=4, line_color=INK_SOFT, line_alpha=0.85)
p.line(T_solid_liquid, P_solid_liquid, line_width=4, line_color=INK_SOFT, line_alpha=0.85)

# Boundary curve labels — italic, secondary ink, theme-adaptive background fill
p.add_layout(
    Label(
        x=215,
        y=150,
        text="Sublimation",
        text_font_size="22pt",
        text_color=INK_MUTED,
        text_font_style="italic",
        angle=0.7,
        background_fill_color=PAGE_BG,
        background_fill_alpha=0.75,
        level="overlay",
    )
)
p.add_layout(
    Label(
        x=382,
        y=3e4,
        text="Vaporization",
        text_font_size="22pt",
        text_color=INK_MUTED,
        text_font_style="italic",
        angle=0.42,
        background_fill_color=PAGE_BG,
        background_fill_alpha=0.75,
        level="overlay",
    )
)
# Melting label repositioned horizontally to avoid cramping against the nearly-vertical curve
p.add_layout(
    Label(
        x=278,
        y=3e7,
        text="Melting",
        text_font_size="22pt",
        text_color=INK_MUTED,
        text_font_style="italic",
        angle=0,
        background_fill_color=PAGE_BG,
        background_fill_alpha=0.75,
        level="overlay",
    )
)

# Triple point — matte red (#AE3030, semantic special condition) with enlarged glow ring
tp_source = ColumnDataSource(
    data={
        "x": [triple_T],
        "y": [triple_P],
        "name": ["Triple Point"],
        "temp": ["273.16 K"],
        "pres": ["611.73 Pa"],
        "desc": ["All three phases coexist"],
    }
)
p.scatter(x="x", y="y", source=tp_source, size=52, color="#AE3030", alpha=0.15, marker="circle")
tp_glyph = p.scatter(
    x="x", y="y", source=tp_source, size=34, color="#AE3030", marker="circle", line_color=PAGE_BG, line_width=3
)

# Critical point — Imprint blue (#4467A3), diamond marker
cp_source = ColumnDataSource(
    data={
        "x": [critical_T],
        "y": [critical_P],
        "name": ["Critical Point"],
        "temp": ["647.1 K"],
        "pres": ["22.064 MPa"],
        "desc": ["Liquid-gas distinction vanishes"],
    }
)
p.scatter(x="x", y="y", source=cp_source, size=56, color="#4467A3", alpha=0.15, marker="diamond")
cp_glyph = p.scatter(
    x="x", y="y", source=cp_source, size=40, color="#4467A3", marker="diamond", line_color=PAGE_BG, line_width=3
)

# HoverTool — Bokeh-specific interactive feature for special points
hover = HoverTool(
    renderers=[tp_glyph, cp_glyph],
    tooltips=[("Point", "@name"), ("Temperature", "@temp"), ("Pressure", "@pres"), ("Significance", "@desc")],
    point_policy="snap_to_data",
)
p.add_tools(hover)

# Span reference lines at critical coordinates — Bokeh distinctive feature
p.add_layout(
    Span(
        location=critical_P, dimension="width", line_color=INK_MUTED, line_width=1.5, line_alpha=0.3, line_dash="dotted"
    )
)
p.add_layout(
    Span(
        location=critical_T,
        dimension="height",
        line_color=INK_MUTED,
        line_width=1.5,
        line_alpha=0.3,
        line_dash="dotted",
    )
)

# Dashed extension above critical point marking where liquid-gas boundary terminates
p.line(
    [critical_T, critical_T], [critical_P, 5e8], line_width=3, line_color=INK_SOFT, line_alpha=0.4, line_dash="dashed"
)

# Phase region labels — bold, INK color, placed within each region
p.add_layout(
    Label(x=205, y=5e6, text="SOLID", text_font_size="30pt", text_color=INK, text_alpha=0.5, text_font_style="bold")
)
p.add_layout(
    Label(x=390, y=5e7, text="LIQUID", text_font_size="30pt", text_color=INK, text_alpha=0.5, text_font_style="bold")
)
p.add_layout(
    Label(x=490, y=700, text="GAS", text_font_size="30pt", text_color=INK, text_alpha=0.5, text_font_style="bold")
)
# Supercritical label repositioned from right edge toward center of the region
p.add_layout(
    Label(
        x=658,
        y=9e7,
        text="SUPERCRITICAL\nFLUID",
        text_font_size="22pt",
        text_color=INK_SOFT,
        text_alpha=0.65,
        text_font_style="bold",
    )
)

# Triple point annotation with arrow
p.add_layout(
    Label(
        x=triple_T + 22,
        y=triple_P * 0.12,
        text="Triple Point\n(273.16 K, 611.73 Pa)",
        text_font_size="18pt",
        text_color="#AE3030",
        text_font_style="bold",
        background_fill_color=ELEVATED_BG,
        background_fill_alpha=0.88,
    )
)
p.add_layout(
    Arrow(
        end=NormalHead(size=12, fill_color="#AE3030", line_color="#AE3030"),
        x_start=triple_T + 22,
        y_start=triple_P * 0.58,
        x_end=triple_T + 2,
        y_end=triple_P * 0.95,
        line_color="#AE3030",
        line_alpha=0.7,
        line_width=2,
    )
)

# Critical point annotation with arrow
p.add_layout(
    Label(
        x=critical_T - 160,
        y=critical_P * 4,
        text="Critical Point\n(647.1 K, 22.06 MPa)",
        text_font_size="18pt",
        text_color="#4467A3",
        text_font_style="bold",
        background_fill_color=ELEVATED_BG,
        background_fill_alpha=0.88,
    )
)
p.add_layout(
    Arrow(
        end=NormalHead(size=12, fill_color="#4467A3", line_color="#4467A3"),
        x_start=critical_T - 55,
        y_start=critical_P * 3.5,
        x_end=critical_T - 2,
        y_end=critical_P * 1.1,
        line_color="#4467A3",
        line_alpha=0.7,
        line_width=2,
    )
)

# Style — Imprint sizing standards (50pt/42pt/34pt) + theme-adaptive chrome
p.title.text_font_size = "50pt"
p.title.text_color = INK
p.title.text_font_style = "bold"

p.xaxis.axis_label_text_font_size = "42pt"
p.yaxis.axis_label_text_font_size = "42pt"
p.xaxis.axis_label_text_color = INK
p.yaxis.axis_label_text_color = INK
p.xaxis.major_label_text_font_size = "34pt"
p.yaxis.major_label_text_font_size = "34pt"
p.xaxis.major_label_text_color = INK_SOFT
p.yaxis.major_label_text_color = INK_SOFT
p.xaxis.axis_line_color = INK_SOFT
p.yaxis.axis_line_color = INK_SOFT
p.xaxis.major_tick_line_color = INK_SOFT
p.yaxis.major_tick_line_color = INK_SOFT

p.xgrid.grid_line_color = INK
p.ygrid.grid_line_color = INK
p.xgrid.grid_line_alpha = 0.12
p.ygrid.grid_line_alpha = 0.12

p.background_fill_color = PAGE_BG
p.border_fill_color = PAGE_BG
p.outline_line_color = INK_SOFT

# Save HTML (interactive catalog artifact) then screenshot via headless Chrome
output_file(f"plot-{THEME}.html")
save(p)

W, H = 3200, 1800
opts = Options()
for arg in (
    "--headless=new",
    "--no-sandbox",
    "--disable-dev-shm-usage",
    "--disable-gpu",
    f"--window-size={W},{H + 200}",
    "--hide-scrollbars",
    "--force-device-scale-factor=1",
):
    opts.add_argument(arg)
driver = webdriver.Chrome(options=opts)
driver.set_window_size(W, H + 200)
driver.get(f"file://{Path(f'plot-{THEME}.html').resolve()}")
time.sleep(3)
raw = driver.get_screenshot_as_png()
driver.quit()
Image.open(io.BytesIO(raw)).crop((0, 0, W, H)).save(f"plot-{THEME}.png")

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

Other implementations