// phase_diagram.js — γ × θ scatter for the paper's empirical panel. // Pure canvas (no Chart.js dependency) so it works offline in Pyodide context. // Data shipped at data/master_gamma_results.json (23 models). const PHASE_DATA_URL = "data/master_gamma_results.json"; let phaseDataCache = null; async function loadPhaseData() { if (phaseDataCache) return phaseDataCache; try { const r = await fetch(PHASE_DATA_URL); if (!r.ok) throw new Error(`HTTP ${r.status}`); phaseDataCache = await r.json(); return phaseDataCache; } catch (e) { console.error("phase_diagram: cannot load", PHASE_DATA_URL, e); return null; } } function gammaPade(theta, T) { return (2 * theta - T * Math.SQRT2) / (2 * theta + T * Math.SQRT2); } function colorForPhase(phase) { if (!phase) return "#888"; if (phase.startsWith("A")) return "#3a8eef"; // blue: deconfined / Phase A if (phase.startsWith("B")) return "#e25555"; // red: confined / Phase B if (phase.indexOf("Hage") >= 0) return "#f0a020"; // amber: Hagedorn return "#888"; } function modelShortName(s) { if (!s) return "?"; // strip org prefix const slash = s.indexOf("/"); return slash >= 0 ? s.substring(slash + 1) : s; } let phaseChartState = { points: [], // {x_log_theta, y_gamma, model, theta, gamma, phase, R2, corpus} hoverIdx: -1, margin: { l: 60, r: 20, t: 20, b: 50 }, }; function renderPhaseDiagram() { const canvas = document.getElementById("phase-canvas"); if (!canvas) return; const ctx = canvas.getContext("2d"); const W = canvas.width; const H = canvas.height; const m = phaseChartState.margin; const plotW = W - m.l - m.r; const plotH = H - m.t - m.b; ctx.clearRect(0, 0, W, H); // axes ranges const xMin = 3, xMax = 7.2; // log10 theta from 1e3 to ~1.6e7 const yMin = 0, yMax = 1.6; const xToPx = (x) => m.l + (x - xMin) / (xMax - xMin) * plotW; const yToPx = (y) => m.t + (1 - (y - yMin) / (yMax - yMin)) * plotH; // Padé curve at T=2000 ctx.strokeStyle = "#444"; ctx.lineWidth = 1.5; ctx.beginPath(); for (let lt = xMin; lt <= xMax; lt += 0.05) { const theta = Math.pow(10, lt); const g = gammaPade(theta, 2000); const px = xToPx(lt); const py = yToPx(g); if (lt === xMin) ctx.moveTo(px, py); else ctx.lineTo(px, py); } ctx.stroke(); // Hagedorn line γ=1 ctx.strokeStyle = "#e25555"; ctx.setLineDash([4, 3]); ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(m.l, yToPx(1)); ctx.lineTo(m.l + plotW, yToPx(1)); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = "#e25555"; ctx.font = "11px sans-serif"; ctx.fillText("Hagedorn γ=1", m.l + plotW - 110, yToPx(1) - 5); // axis lines + labels ctx.strokeStyle = "#666"; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(m.l, m.t); ctx.lineTo(m.l, m.t + plotH); ctx.lineTo(m.l + plotW, m.t + plotH); ctx.stroke(); ctx.fillStyle = "#aaa"; ctx.font = "11px sans-serif"; // x ticks (powers of 10) for (let lt = 3; lt <= 7; lt++) { const px = xToPx(lt); ctx.beginPath(); ctx.moveTo(px, m.t + plotH); ctx.lineTo(px, m.t + plotH + 4); ctx.stroke(); ctx.textAlign = "center"; ctx.fillText(`10^${lt}`, px, m.t + plotH + 18); } ctx.textAlign = "center"; ctx.fillStyle = "#ccc"; ctx.fillText("RoPE θ (log scale)", m.l + plotW / 2, m.t + plotH + 36); // y ticks for (let g = 0; g <= 1.6; g += 0.2) { const py = yToPx(g); ctx.beginPath(); ctx.moveTo(m.l - 4, py); ctx.lineTo(m.l, py); ctx.stroke(); ctx.textAlign = "right"; ctx.fillStyle = "#aaa"; ctx.fillText(g.toFixed(1), m.l - 8, py + 3); } ctx.save(); ctx.translate(15, m.t + plotH / 2); ctx.rotate(-Math.PI / 2); ctx.textAlign = "center"; ctx.fillStyle = "#ccc"; ctx.fillText("γ (measured)", 0, 0); ctx.restore(); // points for (let i = 0; i < phaseChartState.points.length; i++) { const p = phaseChartState.points[i]; const px = xToPx(p.x_log_theta); const py = yToPx(p.y_gamma); ctx.beginPath(); ctx.arc(px, py, i === phaseChartState.hoverIdx ? 8 : 5, 0, Math.PI * 2); ctx.fillStyle = colorForPhase(p.phase); if (p.corpus === "random") { ctx.globalAlpha = 0.4; } else { ctx.globalAlpha = 1.0; } ctx.fill(); ctx.globalAlpha = 1.0; ctx.strokeStyle = i === phaseChartState.hoverIdx ? "#fff" : "#222"; ctx.lineWidth = 1.5; ctx.stroke(); } // legend const legX = m.l + 10; const legY = m.t + 10; const items = [ ["Phase A (γ<1, global)", "#3a8eef"], ["Hagedorn (γ≈1)", "#f0a020"], ["Phase B (γ>1, local)", "#e25555"], ["Padé prediction", "#444"], ]; ctx.font = "11px sans-serif"; for (let i = 0; i < items.length; i++) { const [label, c] = items[i]; ctx.fillStyle = c; ctx.beginPath(); ctx.arc(legX, legY + i * 16, 5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = "#ddd"; ctx.textAlign = "left"; ctx.fillText(label, legX + 10, legY + i * 16 + 4); } } function setupPhasePoints(data) { if (!data) return; const pts = []; for (const x of data) { const theta = x.theta; const gamma = x.gamma_obs; if (!theta || !gamma) continue; pts.push({ x_log_theta: Math.log10(theta), y_gamma: gamma, model: x.model || "?", theta: theta, gamma: gamma, phase: x.phase || "", R2: x.R2 || 0, corpus: x.corpus || "?", }); } phaseChartState.points = pts; } function findHoverPoint(canvas, mx, my) { const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const px = mx * scaleX; const py = my * scaleY; const m = phaseChartState.margin; const plotW = canvas.width - m.l - m.r; const plotH = canvas.height - m.t - m.b; const xMin = 3, xMax = 7.2; const yMin = 0, yMax = 1.6; const xToPx = (x) => m.l + (x - xMin) / (xMax - xMin) * plotW; const yToPx = (y) => m.t + (1 - (y - yMin) / (yMax - yMin)) * plotH; let bestIdx = -1, bestDist = 12; // px tolerance for (let i = 0; i < phaseChartState.points.length; i++) { const p = phaseChartState.points[i]; const ppx = xToPx(p.x_log_theta); const ppy = yToPx(p.y_gamma); const dist = Math.sqrt((px - ppx) ** 2 + (py - ppy) ** 2); if (dist < bestDist) { bestDist = dist; bestIdx = i; } } return bestIdx; } function describePoint(p) { if (!p) return ""; const m = modelShortName(p.model); return `${m}   θ=${p.theta.toLocaleString()}   γ=${p.gamma.toFixed(3)}   R²=${p.R2.toFixed(3)}   phase=${p.phase}   corpus=${p.corpus}`; } export async function initPhaseDiagram() { const canvas = document.getElementById("phase-canvas"); if (!canvas) return; const data = await loadPhaseData(); if (!data) { document.getElementById("phase-info").innerHTML = "Failed to load data/master_gamma_results.json"; return; } setupPhasePoints(data); renderPhaseDiagram(); canvas.addEventListener("mousemove", (e) => { const idx = findHoverPoint(canvas, e.offsetX, e.offsetY); if (idx !== phaseChartState.hoverIdx) { phaseChartState.hoverIdx = idx; renderPhaseDiagram(); const info = document.getElementById("phase-info"); if (info) { info.innerHTML = idx >= 0 ? describePoint(phaseChartState.points[idx]) : "Hover a dot for details. Click to load into Recipe form."; } } }); canvas.addEventListener("click", (e) => { const idx = findHoverPoint(canvas, e.offsetX, e.offsetY); if (idx < 0) return; const p = phaseChartState.points[idx]; // Populate the diag-model field if available, and recipe-section preset const dm = document.getElementById("diag-model"); if (dm) dm.value = p.model; const dt = document.getElementById("diag-theta"); if (dt) dt.value = p.theta; const hf = document.getElementById("hf-id"); if (hf) hf.value = p.model; const info = document.getElementById("phase-info"); if (info) info.innerHTML = `${describePoint(p)}  → loaded into Diagnose & Recipe forms`; }); }