{"spec_id":"phase-diagram-pt","library":"d3","language":"javascript","code":"// anyplot.ai\n// phase-diagram-pt: Thermodynamic Phase Diagram (Pressure-Temperature)\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-08\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\nconst margin = { top: 90, right: 140, bottom: 90, left: 140 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Water phase diagram constants ---\nconst T_TRIPLE = 273.16, P_TRIPLE = 611.73;\nconst T_CRIT = 647.1, P_CRIT = 2.2064e7;\n// Melting slope: negative for water (it expands on freezing)\nconst MELT_SLOPE = -1.346e7;   // Pa/K\nconst L_SUB_R = 6141;          // sublimation Clausius-Clapeyron L/R (K)\n// Vaporization L/R calibrated so the curve terminates exactly at the critical point\nconst L_VAP_R = Math.log(P_CRIT / P_TRIPLE) / (1 / T_TRIPLE - 1 / T_CRIT);\n\n// Unicode superscript digits for axis tick labels\nconst SUP_DIGITS = \"⁰¹²³⁴⁵⁶⁷⁸⁹\";\nconst toSup = n => String(n).split(\"\").map(c => SUP_DIGITS[+c]).join(\"\");\n\n// Axis domain\nconst T_MIN = 170, T_MAX = 720;\nconst P_MIN = 1, P_MAX = 1e9;\n\n// --- Generate phase boundary curves ---\nconst sublimData = [];\nfor (let Tv = 209; Tv <= T_TRIPLE; Tv += 0.3) {\n  sublimData.push({ T: Tv, P: P_TRIPLE * Math.exp(L_SUB_R * (1 / T_TRIPLE - 1 / Tv)) });\n}\n\nconst vapData = [];\nfor (let Tv = T_TRIPLE; Tv <= T_CRIT; Tv += 0.3) {\n  vapData.push({ T: Tv, P: P_TRIPLE * Math.exp(L_VAP_R * (1 / T_TRIPLE - 1 / Tv)) });\n}\n\nconst meltData = [];\nfor (let lP = Math.log10(P_TRIPLE); lP <= 9.15; lP += 0.04) {\n  const P = Math.pow(10, lP);\n  const T = T_TRIPLE + (P - P_TRIPLE) / MELT_SLOPE;\n  if (T >= T_MIN && T <= T_MAX) meltData.push({ T, P });\n}\n\n// --- SVG ---\nconst svg = d3.select(\"#container\").append(\"svg\")\n  .attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales ---\nconst x = d3.scaleLinear().domain([T_MIN, T_MAX]).range([0, iw]);\nconst y = d3.scaleLog().base(10).domain([P_MIN, P_MAX]).range([ih, 0]);\n\n// --- Gridlines ---\n[1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9].forEach(p => {\n  g.append(\"line\")\n    .attr(\"x1\", 0).attr(\"x2\", iw).attr(\"y1\", y(p)).attr(\"y2\", y(p))\n    .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n});\n[200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700].forEach(temp => {\n  g.append(\"line\")\n    .attr(\"x1\", x(temp)).attr(\"x2\", x(temp)).attr(\"y1\", 0).attr(\"y2\", ih)\n    .attr(\"stroke\", t.grid).attr(\"stroke-width\", 1);\n});\n\n// --- Axes ---\nconst xAx = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x)\n    .tickValues([200, 250, 300, 350, 400, 450, 500, 550, 600, 650, 700])\n    .tickSize(6));\nxAx.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nxAx.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nxAx.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\nconst yAx = g.append(\"g\")\n  .call(d3.axisLeft(y)\n    .tickValues([1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9])\n    .tickFormat(d => \"10\" + toSup(Math.round(Math.log10(d))))\n    .tickSize(6));\nyAx.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\nyAx.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nyAx.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\n// --- Clip path for boundary curves ---\ng.append(\"clipPath\").attr(\"id\", \"plot-clip\")\n  .append(\"rect\").attr(\"width\", iw).attr(\"height\", ih);\n\n// --- Line generator ---\nconst mkLine = d3.line()\n  .x(d => x(d.T))\n  .y(d => y(d.P))\n  .defined(d => d.T >= T_MIN && d.T <= T_MAX && d.P >= P_MIN && d.P <= P_MAX);\n\nconst curves = g.append(\"g\").attr(\"clip-path\", \"url(#plot-clip)\");\n\n// Imprint palette order: solid-liquid first (#009E73), then liquid-gas, then solid-gas\nconst boundaries = [\n  { data: meltData,   color: t.palette[0], dash: null,  strokeW: 3.5 },  // #009E73 solid-liquid\n  { data: vapData,    color: t.palette[1], dash: null,  strokeW: 3.5 },  // #C475FD liquid-gas\n  { data: sublimData, color: t.palette[2], dash: \"8,4\", strokeW: 3   },  // #4467A3 solid-gas\n];\nboundaries.forEach(b => {\n  const path = curves.append(\"path\").datum(b.data)\n    .attr(\"fill\", \"none\")\n    .attr(\"stroke\", b.color)\n    .attr(\"stroke-width\", b.strokeW)\n    .attr(\"d\", mkLine);\n  if (b.dash) path.attr(\"stroke-dasharray\", b.dash);\n});\n\n// --- Triple point and critical point markers ---\nconst addMarker = (cx, cy, fill) =>\n  g.append(\"circle\").attr(\"cx\", cx).attr(\"cy\", cy).attr(\"r\", 9)\n    .attr(\"fill\", fill).attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 3);\n\naddMarker(x(T_TRIPLE), y(P_TRIPLE), t.ink);\naddMarker(x(T_CRIT),   y(P_CRIT),   t.palette[3]);  // ochre #BD8233\n\n// --- Point annotations ---\nconst tpX = x(T_TRIPLE), tpY = y(P_TRIPLE);\ng.append(\"text\").attr(\"x\", tpX + 14).attr(\"y\", tpY - 12)\n  .attr(\"fill\", t.ink).style(\"font-size\", \"14px\").style(\"font-weight\", \"600\")\n  .text(\"Triple point\");\ng.append(\"text\").attr(\"x\", tpX + 14).attr(\"y\", tpY + 6)\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\")\n  .text(\"273.16 K · 612 Pa\");\n\nconst cpX = x(T_CRIT), cpY = y(P_CRIT);\ng.append(\"text\").attr(\"x\", cpX - 14).attr(\"y\", cpY - 12)\n  .attr(\"text-anchor\", \"end\").attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\").style(\"font-weight\", \"600\")\n  .text(\"Critical point\");\ng.append(\"text\").attr(\"x\", cpX - 14).attr(\"y\", cpY + 6)\n  .attr(\"text-anchor\", \"end\").attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text(\"647.1 K · 22.1 MPa\");\n\n// --- Phase region labels ---\n// Positions verified to lie within each phase region\n[\n  { T: 225,  P: 5e5,  anchor: \"middle\", text: \"SOLID\"          },\n  { T: 390,  P: 3e5,  anchor: \"middle\", text: \"LIQUID\"         },\n  { T: 490,  P: 200,  anchor: \"middle\", text: \"GAS\"            },\n].forEach(r => {\n  g.append(\"text\")\n    .attr(\"x\", x(r.T)).attr(\"y\", y(r.P))\n    .attr(\"text-anchor\", r.anchor)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"16px\").style(\"font-weight\", \"700\")\n    .style(\"font-style\", \"italic\").style(\"opacity\", \"0.85\")\n    .text(r.text);\n});\n\n// Supercritical fluid label as two tspan lines (combined to avoid disconnect)\nconst scfX = x(676), scfY = y(1.3e8);\nconst scfEl = g.append(\"text\")\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\").style(\"font-weight\", \"700\")\n  .style(\"font-style\", \"italic\").style(\"opacity\", \"0.85\");\nscfEl.append(\"tspan\").attr(\"x\", scfX).attr(\"y\", scfY).text(\"SUPERCRITICAL\");\nscfEl.append(\"tspan\").attr(\"x\", scfX).attr(\"dy\", \"1.3em\").text(\"FLUID\");\n\n// --- Legend (placed in open gas region, lower right) ---\nconst lx = iw - 20;\nconst ly0 = ih - 5 * 28 - 20;\n\nconst legCurves = [\n  { color: t.palette[0], label: \"Solid–Liquid (Melting)\",     dash: null  },\n  { color: t.palette[1], label: \"Liquid–Gas (Vaporization)\",  dash: null  },\n  { color: t.palette[2], label: \"Solid–Gas (Sublimation)\",    dash: \"6,3\" },\n];\nlegCurves.forEach((d, i) => {\n  const ly = ly0 + i * 28;\n  const seg = g.append(\"line\")\n    .attr(\"x1\", lx - 28).attr(\"x2\", lx).attr(\"y1\", ly).attr(\"y2\", ly)\n    .attr(\"stroke\", d.color).attr(\"stroke-width\", 3.5);\n  if (d.dash) seg.attr(\"stroke-dasharray\", d.dash);\n  g.append(\"text\").attr(\"x\", lx - 34).attr(\"y\", ly + 5)\n    .attr(\"text-anchor\", \"end\").attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\").text(d.label);\n});\n\nconst legPoints = [\n  { fill: t.ink,        label: \"Triple point\"   },\n  { fill: t.palette[3], label: \"Critical point\" },\n];\nlegPoints.forEach((d, i) => {\n  const ly = ly0 + (legCurves.length + i) * 28;\n  g.append(\"circle\").attr(\"cx\", lx - 14).attr(\"cy\", ly).attr(\"r\", 6)\n    .attr(\"fill\", d.fill).attr(\"stroke\", t.pageBg).attr(\"stroke-width\", 2);\n  g.append(\"text\").attr(\"x\", lx - 24).attr(\"y\", ly + 5)\n    .attr(\"text-anchor\", \"end\").attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\").text(d.label);\n});\n\n// --- Axis labels ---\nsvg.append(\"text\")\n  .attr(\"x\", margin.left + iw / 2).attr(\"y\", height - 22)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink).style(\"font-size\", \"16px\")\n  .text(\"Temperature (K)\");\n\nsvg.append(\"text\")\n  .attr(\"transform\", `translate(30,${margin.top + ih / 2})rotate(-90)`)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink).style(\"font-size\", \"16px\")\n  .text(\"Pressure (Pa)\");\n\n// --- Title ---\nconst TITLE = \"Water Phase Diagram · phase-diagram-pt · javascript · d3 · anyplot.ai\";\nconst titleFs = Math.max(14, Math.round(22 * Math.min(1.0, 67 / TITLE.length)));\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\").attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleFs}px`).style(\"font-weight\", \"600\")\n  .text(TITLE);\n"}