{
// ==========================================================================
// 1. SCIENTIFIC HELPER MODULE
// ==========================================================================
const Sci = (() => {
// --- Physical Constants for Water ---
const T_c = 647.096; const P_c_bar = 220.64; const T_tp = 273.16; const P_tp_bar = 0.00611657; const slope_K_per_bar = -0.0075;
function p_vaporization(T) { if (T < T_tp || T > T_c) return NaN; const theta = 1 - T / T_c; const c = [-7.85951783, 1.84408259, -11.7866497, 22.6807411, -15.9618719, 1.80122502]; const exponent = (T_c / T) * (c[0] * theta + c[1] * Math.pow(theta, 1.5) + c[2] * Math.pow(theta, 3) + c[3] * Math.pow(theta, 3.5) + c[4] * Math.pow(theta, 4) + c[5] * Math.pow(theta, 7.5)); return P_c_bar * Math.exp(exponent); }
function p_sublimation(T) { if (T >= T_tp) return NaN; const theta = T / T_tp; const a = [-13.928169, 34.707823]; const exponent = a[0] * (1 - Math.pow(theta, -1.5)) + a[1] * (1 - Math.pow(theta, -1.25)); return P_tp_bar * Math.exp(exponent); }
function t_fusion(P_bar) { if (P_bar < P_tp_bar) return T_tp; return T_tp + (P_bar - P_tp_bar) * slope_K_per_bar; }
function isNearTriplePoint(T, P) { const temp_close = Math.abs(T - T_tp) < 2.0; const press_close = Math.abs(Math.log(P) - Math.log(P_tp_bar)) < 0.5; return temp_close && press_close; }
function isNearCriticalPoint(T, P_bar) { const temp_close = Math.abs(T - T_c) < 5.0; const press_close = Math.abs(P_bar - P_c_bar) < 10.0; return temp_close && press_close; }
function getPhase(T, P_bar) { if (isNearCriticalPoint(T, P_bar)) return 'Critical Point'; if (isNearTriplePoint(T, P_bar)) return 'Triple Point'; if (T > T_c && P_bar > P_c_bar) return 'Supercritical Fluid'; if (T > T_c) return 'Gas'; const p_vap = p_vaporization(T); if (!isNaN(p_vap)) { return P_bar > p_vap ? (T < t_fusion(P_bar) ? 'Solid' : 'Liquid') : 'Gas'; } const p_sub = p_sublimation(T); if (!isNaN(p_sub)) { return P_bar > p_sub ? 'Solid' : 'Gas'; } return T < t_fusion(P_bar) ? 'Solid' : 'Liquid'; }
return { T_c, P_c_bar, T_tp, P_tp_bar, getPhase, p_vaporization, p_sublimation, t_fusion };
})();
// ==========================================================================
// 2. WIDGET LAYOUT, STYLES, AND UI ELEMENT CREATION
// ==========================================================================
const vizSize = 600;
const t_min = 0, t_max = 1000;
const p_min = 0.001, p_max = 1000;
const log_p_min = Math.log(p_min), log_p_max = Math.log(p_max);
const uniqueId = `water-plot-only-${crypto.randomUUID()}`;
const widgetContainer = html`
<style>
#${uniqueId} .control-group { display: flex; flex-direction: column; gap: 8px; }
#${uniqueId} .control-group label { font-weight: 500; font-size: 0.9em; color: #333; }
#${uniqueId} .control-group input[type=number] { width: 100%; box-sizing: border-box; border: none; border-bottom: 1.5px solid #ccc; border-radius: 0; background-color: transparent; padding: 8px 4px; font-size: 1em; transition: border-color 0.3s; }
#${uniqueId} .control-group input[type=number]:focus { border-color: #007bff; outline: none; }
#${uniqueId} .control-group input[type=range] { width: 100%; }
#${uniqueId} .presets { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
#${uniqueId} .preset-button { background: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; padding: 6px 12px; font-size: 0.9em; cursor: pointer; transition: background-color 0.2s, border-color 0.2s; }
#${uniqueId} .preset-button:hover { background-color: #e0e0e0; }
#${uniqueId} .preset-button.active { background-color: #007bff; color: white; border-color: #007bff; }
#${uniqueId} .boundary-line { fill: none; stroke: #007bff; stroke-width: 2.5; transition: stroke-width 0.2s; }
#${uniqueId} .boundary-line.highlight { stroke-width: 5; }
</style>
<div id="${uniqueId}" style="display: flex; flex-direction: column; gap: 20px; font-family: system-ui, sans-serif; max-width: 1200px; margin: auto;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; max-width: ${vizSize}px; margin: auto;">
<div class="control-group">
<label for="temp-num-${uniqueId}">Temperature (K)</label>
<input id="temp-num-${uniqueId}" type="number" min="${t_min}" max="${t_max}" step="0.1" value="298.15">
<input id="temp-slider-${uniqueId}" type="range" min="${t_min}" max="${t_max}" step="0.1" value="298.15">
</div>
<div class="control-group">
<label for="pressure-num-${uniqueId}">External Pressure (bar)</label>
<input id="pressure-num-${uniqueId}" type="number" min="${p_min}" max="${p_max}" step="any" value="1.01325">
<input id="pressure-slider-${uniqueId}" type="range" min="${log_p_min}" max="${log_p_max}" step="0.01" value="${Math.log(1.01325)}">
</div>
</div>
<div class="presets">
<button class="preset-button" data-point="ambient">Ambient</button>
<button class="preset-button" data-point="freezing">Normal Freezing</button>
<button class="preset-button" data-point="boiling">Normal Boiling</button>
<button class="preset-button" data-point="triple">Triple Pt</button>
<button class="preset-button" data-point="critical">Critical Pt</button>
</div>
<div style="display: flex; justify-content: center;">
<div id="plot-container-${uniqueId}"></div>
</div>
</div>`;
const tempNum = widgetContainer.querySelector(`#temp-num-${uniqueId}`);
const tempSlider = widgetContainer.querySelector(`#temp-slider-${uniqueId}`);
const pressureNum = widgetContainer.querySelector(`#pressure-num-${uniqueId}`);
const pressureSlider = widgetContainer.querySelector(`#pressure-slider-${uniqueId}`);
const presetButtons = widgetContainer.querySelectorAll(".preset-button");
const plotContainer = widgetContainer.querySelector(`#plot-container-${uniqueId}`);
// ==========================================================================
// 3. D3 PLOT SETUP
// ==========================================================================
const margin = {top: 50, right: 60, bottom: 50, left: 60};
const width = vizSize, height = vizSize;
const x = d3.scaleLinear().domain([t_min, t_max]).range([margin.left, width - margin.right]);
const y = d3.scaleLog().domain([p_min, p_max]).range([height - margin.bottom, margin.top]);
const svg = d3.create("svg").attr("width", width).attr("height", height).attr("viewBox", [0, 0, width, height]).style("max-width", "100%").style("height", "auto");
plotContainer.appendChild(svg.node());
const xAxisGroup = svg.append("g").attr("transform", `translate(0,${height - margin.bottom})`).call(d3.axisBottom(x));
xAxisGroup.select(".domain").remove();
xAxisGroup.selectAll(".tick line").clone().attr("y2", -height + margin.top + margin.bottom).attr("stroke-opacity", 0.1);
xAxisGroup.append("text").attr("x", width / 2).attr("y", margin.bottom - 10).attr("fill", "currentColor").attr("text-anchor", "middle").text("Temperature (K)");
const yAxisGroup = svg.append("g").attr("transform", `translate(${margin.left},0)`).call(d3.axisLeft(y).ticks(null, "~e"));
yAxisGroup.select(".domain").remove();
yAxisGroup.selectAll(".tick line").clone().attr("x2", width - margin.left - margin.right).attr("stroke-opacity", 0.1);
yAxisGroup.append("text").attr("transform", "rotate(-90)").attr("x", -height / 2).attr("y", -margin.left + 15).attr("fill", "currentColor").attr("text-anchor", "middle").text("Pressure (bar)");
const x_celsius = d3.scaleLinear().domain([t_min - 273.15, t_max - 273.15]).range(x.range());
const xAxisTopGroup = svg.append("g").attr("transform", `translate(0, ${margin.top})`).call(d3.axisTop(x_celsius));
xAxisTopGroup.select(".domain").remove();
xAxisTopGroup.append("text").attr("x", width / 2).attr("y", -margin.top + 15).attr("fill", "currentColor").attr("text-anchor", "middle").text("Temperature (°C)");
const y_pascal = d3.scaleLog().domain([p_min * 1e5, p_max * 1e5]).range(y.range());
const yAxisRightGroup = svg.append("g").attr("transform", `translate(${width - margin.right}, 0)`).call(d3.axisRight(y_pascal).ticks(null, "~e"));
yAxisRightGroup.select(".domain").remove();
yAxisRightGroup.append("text").attr("transform", "rotate(90)").attr("x", height / 2).attr("y", -margin.right + 20).attr("fill", "currentColor").attr("text-anchor", "middle").text("Pressure (Pa)");
const lineGen = d3.line().x(d => x(d[0])).y(d => y(d[1]));
const vapLineData = d3.range(Sci.T_tp, Sci.T_c, 1).map(t => [t, Sci.p_vaporization(t)]);
vapLineData.push([Sci.T_c, Sci.P_c_bar]);
const subLineData = d3.range(200, Sci.T_tp, 0.5).map(t => [t, Sci.p_sublimation(t)]).filter(d => d && d[1] >= p_min);
const fusionLineData = d3.range(Sci.P_tp_bar, p_max + 1, 1).map(p => [Sci.t_fusion(p), p]);
const clipPathId = `plot-area-clip-${uniqueId}`;
const clipPath = svg.append("defs").append("clipPath").attr("id", clipPathId).append("rect").attr("x", margin.left).attr("y", margin.top).attr("width", width - margin.left - margin.right).attr("height", height - margin.top - margin.bottom);
const shadedRegions = svg.append("g").attr("clip-path", `url(#${clipPathId})`);
const fullAreaPath = [[t_min, p_min], [t_max, p_min], [t_max, p_max], [t_min, p_max]];
const solidPath = [...subLineData, ...fusionLineData, [t_min, p_max], [t_min, p_min]];
const liquidPath = [...vapLineData, [Sci.T_c, p_max], [fusionLineData[fusionLineData.length-1][0], p_max], ...fusionLineData.reverse()];
const supercriticalPath = [[Sci.T_c, Sci.P_c_bar], [t_max, Sci.P_c_bar], [t_max, p_max], [Sci.T_c, p_max]];
shadedRegions.append("path").datum(fullAreaPath).attr("d", lineGen).attr("fill", "rgba(200, 200, 200, 0.15)");
shadedRegions.append("path").datum(solidPath).attr("d", lineGen).attr("fill", "rgba(100, 100, 200, 0.1)");
shadedRegions.append("path").datum(liquidPath).attr("d", lineGen).attr("fill", "rgba(100, 200, 100, 0.1)");
shadedRegions.append("path").datum(supercriticalPath).attr("d", lineGen).attr("fill", "rgba(255, 165, 0, 0.15)");
const vapPath = svg.append("path").datum(vapLineData).attr("class", "boundary-line").attr("d", lineGen);
const subPath = svg.append("path").datum(subLineData).attr("class", "boundary-line").attr("d", lineGen);
const fusPath = svg.append("path").datum(fusionLineData).attr("class", "boundary-line").attr("d", lineGen);
svg.append("circle").attr("cx", x(Sci.T_tp)).attr("cy", y(Sci.P_tp_bar)).attr("r", 5).attr("fill", "black");
svg.append("circle").attr("cx", x(Sci.T_c)).attr("cy", y(Sci.P_c_bar)).attr("r", 5).attr("fill", "black");
svg.append("text").attr("x", x(Sci.T_tp) + 10).attr("y", y(Sci.P_tp_bar)).text("Triple Point").style("font-size", "12px").attr("fill", "black").attr("dominant-baseline", "middle");
svg.append("text").attr("x", x(Sci.T_c) + 10).attr("y", y(Sci.P_c_bar)).text("Critical Point").style("font-size", "12px").attr("fill", "black").attr("dominant-baseline", "middle");
const phaseText = svg.append("text").attr("x", (width + margin.left - margin.right) / 2).attr("y", (height + margin.top - margin.bottom) / 2).attr("text-anchor", "middle").attr("dominant-baseline", "middle").style("font-size", "4em").style("font-weight", "bold").style("fill", "rgba(0, 0, 0, 0.15)").style("pointer-events", "none");
const marker = svg.append("circle").attr("r", 6).attr("fill", "red").attr("stroke", "white").attr("stroke-width", 1.5);
// ==========================================================================
// 4. REACTIVITY
// ==========================================================================
let currentT = tempNum.valueAsNumber;
let currentP = pressureNum.valueAsNumber;
const PRESET_POINTS = { ambient: { T: 298.15, P: 1.01325 }, freezing: { T: 273.15, P: 1.01325 }, boiling: { T: 373.15, P: 1.01325 }, triple: { T: Sci.T_tp, P: Sci.P_tp_bar }, critical: { T: Sci.T_c, P: Sci.P_c_bar } };
function setValues(T, P) { currentT = T; currentP = P; tempNum.value = T.toFixed(2); tempSlider.value = T; pressureNum.value = P.toPrecision(4); pressureSlider.value = Math.log(P); updateVisualization(); }
function updateVisualization() { const phase = Sci.getPhase(currentT, currentP); phaseText.text(null); if (phase === 'Supercritical Fluid') { const xPos = (width + margin.left - margin.right) / 2; phaseText.append("tspan").attr("x", xPos).attr("dy", "-0.6em").text("Supercritical"); phaseText.append("tspan").attr("x", xPos).attr("dy", "1.2em").text("Fluid"); } else if (phase === 'Critical Point') { const xPos = (width + margin.left - margin.right) / 2; phaseText.append("tspan").attr("x", xPos).attr("dy", "-0.6em").text("Critical"); phaseText.append("tspan").attr("x", xPos).attr("dy", "1.2em").text("Point"); } else { phaseText.text(phase); } marker.transition().duration(250).attr("cx", x(currentT)).attr("cy", y(currentP)); }
function debounce(func, delay) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; }
const debouncedUpdate = debounce(() => { currentT = tempNum.valueAsNumber; currentP = pressureNum.valueAsNumber; updateVisualization(); }, 50);
const plotOverlay = svg.append("rect").attr("x", margin.left).attr("y", margin.top).attr("width", width - margin.left - margin.right).attr("height", height - margin.top - margin.bottom).style("fill", "none").style("pointer-events", "all").style("cursor", "crosshair");
const tooltip = svg.append("text").attr("x", 0).attr("y", 0).style("font-size", "12px").style("pointer-events", "none").style("text-anchor", "middle").style("fill", "black").style("display", "none");
plotOverlay.on("mousemove", (event) => { const [mx, my] = d3.pointer(event); if (mx > margin.left && mx < width - margin.right && my > margin.top && my < height - margin.bottom) { const T = x.invert(mx); const P = y.invert(my); tooltip.style("display", "block").attr("x", mx).attr("y", my - 10).text(`T: ${T.toFixed(1)} K, P: ${P.toPrecision(3)} bar`); const distToVap = d3.least(vapLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); const distToSub = d3.least(subLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); const distToFus = d3.least(fusionLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); vapPath.classed("highlight", distToVap < 10); subPath.classed("highlight", distToSub < 10); fusPath.classed("highlight", distToFus < 10); } else { tooltip.style("display", "none"); } }).on("mouseleave", () => { tooltip.style("display", "none"); [vapPath, subPath, fusPath].forEach(p => p.classed("highlight", false)); }).on("click", (event) => { const [mx, my] = d3.pointer(event); const T = x.invert(mx); const P = y.invert(my); handleManualInput(); setValues(T, P); });
function handleManualInput() { presetButtons.forEach(b => b.classList.remove('active')); }
tempSlider.addEventListener('input', () => { tempNum.value = tempSlider.value; debouncedUpdate(); });
tempNum.addEventListener('input', () => { tempSlider.value = tempNum.value; debouncedUpdate(); });
pressureSlider.addEventListener('input', () => { const p_val = Math.exp(pressureSlider.valueAsNumber); pressureNum.value = p_val.toPrecision(4); debouncedUpdate(); });
pressureNum.addEventListener('input', () => { const p_val = pressureNum.valueAsNumber; if (p_val > 0) { pressureSlider.value = Math.log(p_val); } debouncedUpdate(); });
presetButtons.forEach(button => {
button.addEventListener('click', () => {
if (button.classList.contains('active')) {
button.classList.remove('active');
} else {
presetButtons.forEach(b => b.classList.remove('active'));
button.classList.add('active');
const point = PRESET_POINTS[button.dataset.point];
// *** BUG FIX IS HERE: Changed 'P' to 'point.P' ***
setValues(point.T, point.P);
}
});
});
updateVisualization();
// ==========================================================================
// 5. RETURN THE FINAL WIDGET
// ==========================================================================
return widgetContainer;
}
Phase Diagrams
p-T Diagrams for some substances
Water, H2O
Carbon dioxide, CO2
{
// ==========================================================================
// 1. SCIENTIFIC HELPER MODULE FOR CARBON DIOXIDE (CO2)
// ==========================================================================
const Sci = (() => {
// --- Physical Constants for Carbon Dioxide ---
const T_c = 304.1282; const P_c_bar = 73.773; const T_tp = 216.592; const P_tp_bar = 5.185;
function p_vaporization(T) { if (T < T_tp || T > T_c) return NaN; const theta = 1 - T / T_c; const c = [ -7.0604356, 1.9349897, -1.653433, -2.474942, -5.556825 ]; const exponent = (T_c / T) * (c[0] * theta + c[1] * Math.pow(theta, 1.5) + c[2] * Math.pow(theta, 2) + c[3] * Math.pow(theta, 2.5) + c[4] * Math.pow(theta, 5)); return P_c_bar * Math.exp(exponent); }
function p_sublimation(T) { if (T >= T_tp) return NaN; const theta = T / T_tp; const c = [ -14.86316, 2.4418258 ]; const exponent = c[0] * (1 - theta) + c[1] * Math.log(theta); return P_tp_bar * Math.exp(exponent); }
function t_fusion(P_bar) { if (P_bar < P_tp_bar) return NaN; const A = 1966.5, C = 1.74; return T_tp * Math.pow(((P_bar - P_tp_bar) / A) + 1, 1 / C); }
function getPhase(T, P_bar) { const isNearCriticalPoint = Math.abs(T - T_c) < 5.0 && Math.abs(P_bar - P_c_bar) < 5.0; if (isNearCriticalPoint) return 'Critical Point'; const isNearTriplePoint = Math.abs(T - T_tp) < 2.0 && Math.abs(Math.log(P_bar) - Math.log(P_tp_bar)) < 0.3; if (isNearTriplePoint) return 'Triple Point'; const t_fus = t_fusion(P_bar); if (!isNaN(t_fus) && T < t_fus) return 'Solid'; if (T < T_tp) { const p_sub = p_sublimation(T); if (!isNaN(p_sub)) { return P_bar > p_sub ? 'Solid' : 'Gas'; } return 'Solid'; } if (T > T_c && P_bar > P_c_bar) return 'Supercritical Fluid'; if (T > T_c) return 'Gas'; const p_vap = p_vaporization(T); if (!isNaN(p_vap)) { return P_bar > p_vap ? 'Liquid' : 'Gas'; } return 'Liquid'; }
return { T_c, P_c_bar, T_tp, P_tp_bar, getPhase, p_vaporization, p_sublimation, t_fusion };
})();
// ==========================================================================
// 2. WIDGET LAYOUT, STYLES, AND UI ELEMENT CREATION
// ==========================================================================
const vizSize = 600;
const t_min = 0, t_max = 1000;
const p_min = 0.001, p_max = 1000;
const log_p_min = Math.log(p_min), log_p_max = Math.log(p_max);
const uniqueId = `co2-phase-diagram-${crypto.randomUUID()}`;
const widgetContainer = html`
<style>
#${uniqueId} .control-group { display: flex; flex-direction: column; gap: 8px; }
#${uniqueId} .control-group label { font-weight: 500; font-size: 0.9em; color: #333; }
#${uniqueId} .control-group input[type=number] { width: 100%; box-sizing: border-box; border: none; border-bottom: 1.5px solid #ccc; border-radius: 0; background-color: transparent; padding: 8px 4px; font-size: 1em; transition: border-color 0.3s; }
#${uniqueId} .control-group input[type=number]:focus { border-color: #007bff; outline: none; }
#${uniqueId} .control-group input[type=range] { width: 100%; }
#${uniqueId} .presets { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
#${uniqueId} .preset-button { background: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; padding: 6px 12px; font-size: 0.9em; cursor: pointer; transition: background-color 0.2s, border-color 0.2s; }
#${uniqueId} .preset-button:hover { background-color: #e0e0e0; }
#${uniqueId} .preset-button.active { background-color: #007bff; color: white; border-color: #007bff; }
#${uniqueId} .boundary-line { fill: none; stroke: #007bff; stroke-width: 2.5; transition: stroke-width 0.2s; }
#${uniqueId} .boundary-line.highlight { stroke-width: 5; }
</style>
<div id="${uniqueId}" style="display: flex; flex-direction: column; gap: 20px; font-family: system-ui, sans-serif; max-width: 1100px; margin: auto;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; max-width: ${vizSize}px; margin: auto;">
<div class="control-group">
<label for="temp-num-${uniqueId}">Temperature (K)</label>
<input id="temp-num-${uniqueId}" type="number" min="${t_min}" max="${t_max}" step="0.1" value="298.15">
<input id="temp-slider-${uniqueId}" type="range" min="${t_min}" max="${t_max}" step="0.1" value="298.15">
</div>
<div class="control-group">
<label for="pressure-num-${uniqueId}">External Pressure (bar)</label>
<input id="pressure-num-${uniqueId}" type="number" min="${p_min}" max="${p_max}" step="any" value="1.0135">
<input id="pressure-slider-${uniqueId}" type="range" min="${log_p_min}" max="${log_p_max}" step="0.01" value="${Math.log(1.0135)}">
</div>
</div>
<div class="presets">
<button class="preset-button" data-point="ambient">Ambient</button>
<button class="preset-button" data-point="sublimation">Normal Sublimation</button>
<button class="preset-button" data-point="triple">Triple Pt</button>
<button class="preset-button" data-point="critical">Critical Pt</button>
</div>
<div style="display: flex; justify-content: center;">
<div id="plot-container-${uniqueId}"></div>
</div>
</div>`;
const tempNum = widgetContainer.querySelector(`#temp-num-${uniqueId}`);
const tempSlider = widgetContainer.querySelector(`#temp-slider-${uniqueId}`);
const pressureNum = widgetContainer.querySelector(`#pressure-num-${uniqueId}`);
const pressureSlider = widgetContainer.querySelector(`#pressure-slider-${uniqueId}`);
const presetButtons = widgetContainer.querySelectorAll(".preset-button");
const plotContainer = widgetContainer.querySelector(`#plot-container-${uniqueId}`);
// ==========================================================================
// 3. D3 PLOT SETUP
// ==========================================================================
const margin = {top: 50, right: 60, bottom: 50, left: 60};
const width = vizSize, height = vizSize;
const x = d3.scaleLinear().domain([t_min, t_max]).range([margin.left, width - margin.right]);
const y = d3.scaleLog().domain([p_min, p_max]).range([height - margin.bottom, margin.top]);
const svg = d3.create("svg").attr("width", width).attr("height", height).attr("viewBox", [0, 0, width, height]).style("max-width", "100%").style("height", "auto");
plotContainer.appendChild(svg.node());
const xAxisGroup = svg.append("g").attr("transform", `translate(0,${height - margin.bottom})`).call(d3.axisBottom(x));
xAxisGroup.select(".domain").remove();
xAxisGroup.selectAll(".tick line").clone().attr("y2", -height + margin.top + margin.bottom).attr("stroke-opacity", 0.1);
xAxisGroup.append("text").attr("x", width / 2).attr("y", margin.bottom - 10).attr("fill", "currentColor").attr("text-anchor", "middle").text("Temperature (K)");
const yAxisGroup = svg.append("g").attr("transform", `translate(${margin.left},0)`).call(d3.axisLeft(y).ticks(null, "~e"));
yAxisGroup.select(".domain").remove();
yAxisGroup.selectAll(".tick line").clone().attr("x2", width - margin.left - margin.right).attr("stroke-opacity", 0.1);
yAxisGroup.append("text").attr("transform", "rotate(-90)").attr("x", -height / 2).attr("y", -margin.left + 15).attr("fill", "currentColor").attr("text-anchor", "middle").text("Pressure (bar)");
const x_celsius = d3.scaleLinear().domain([t_min - 273.15, t_max - 273.15]).range(x.range());
const xAxisTopGroup = svg.append("g").attr("transform", `translate(0, ${margin.top})`).call(d3.axisTop(x_celsius));
xAxisTopGroup.select(".domain").remove();
xAxisTopGroup.append("text").attr("x", width / 2).attr("y", -margin.top + 15).attr("fill", "currentColor").attr("text-anchor", "middle").text("Temperature (°C)");
const y_pascal = d3.scaleLog().domain([p_min * 1e5, p_max * 1e5]).range(y.range());
const yAxisRightGroup = svg.append("g").attr("transform", `translate(${width - margin.right}, 0)`).call(d3.axisRight(y_pascal).ticks(null, "~e"));
yAxisRightGroup.select(".domain").remove();
yAxisRightGroup.append("text").attr("transform", "rotate(90)").attr("x", height / 2).attr("y", -margin.right + 20).attr("fill", "currentColor").attr("text-anchor", "middle").text("Pressure (Pa)");
const lineGen = d3.line().x(d => x(d[0])).y(d => y(d[1]));
const vapLineData = d3.range(Sci.T_tp, Sci.T_c, 0.5).map(t => [t, Sci.p_vaporization(t)]);
vapLineData.push([Sci.T_c, Sci.P_c_bar]);
const subLineData = d3.range(t_min, Sci.T_tp, 0.5).map(t => [t, Sci.p_sublimation(t)]).filter(d => d && d[1] >= p_min);
if (subLineData.length > 0) {
subLineData.unshift([subLineData[0][0], p_min]);
}
subLineData.push([Sci.T_tp, Sci.P_tp_bar]);
const fusionLineData = d3.range(Sci.P_tp_bar, p_max + 5, 5).map(p => [Sci.t_fusion(p), p]).filter(d => d && !isNaN(d[0]) && d[0] <= t_max);
fusionLineData.unshift([Sci.T_tp, Sci.P_tp_bar]);
const clipPathId = `plot-area-clip-${uniqueId}`;
const clipPath = svg.append("defs").append("clipPath").attr("id", clipPathId).append("rect").attr("x", margin.left).attr("y", margin.top).attr("width", width - margin.left - margin.right).attr("height", height - margin.top - margin.bottom);
const shadedRegions = svg.append("g").attr("clip-path", `url(#${clipPathId})`);
const fullAreaPath = [[t_min, p_min], [t_max, p_min], [t_max, p_max], [t_min, p_max]];
const solidPath = [...subLineData, ...fusionLineData, [t_max, p_max], [t_min, p_max], [t_min, p_min]];
// *** BUG FIX IS HERE: Corrected path definitions ***
const liquidPath = [...vapLineData.slice().reverse(), ...fusionLineData, [t_max, p_max], [Sci.T_c, p_max]];
const supercriticalPath = [[Sci.T_c, Sci.P_c_bar], [t_max, Sci.P_c_bar], [t_max, p_max], [Sci.T_c, p_max]];
shadedRegions.append("path").datum(fullAreaPath).attr("d", lineGen).attr("fill", "rgba(200, 200, 200, 0.15)");
shadedRegions.append("path").datum(solidPath).attr("d", lineGen).attr("fill", "rgba(100, 100, 200, 0.1)");
shadedRegions.append("path").datum(liquidPath).attr("d", lineGen).attr("fill", "rgba(100, 200, 100, 0.1)");
shadedRegions.append("path").datum(supercriticalPath).attr("d", lineGen).attr("fill", "rgba(255, 165, 0, 0.15)");
const vapPath = svg.append("path").datum(vapLineData).attr("class", "boundary-line").attr("d", lineGen);
const subPath = svg.append("path").datum(subLineData).attr("class", "boundary-line").attr("d", lineGen);
const fusPath = svg.append("path").datum(fusionLineData).attr("class", "boundary-line").attr("d", lineGen);
svg.append("circle").attr("cx", x(Sci.T_tp)).attr("cy", y(Sci.P_tp_bar)).attr("r", 5).attr("fill", "black");
svg.append("circle").attr("cx", x(Sci.T_c)).attr("cy", y(Sci.P_c_bar)).attr("r", 5).attr("fill", "black");
svg.append("text").attr("x", x(Sci.T_tp) + 10).attr("y", y(Sci.P_tp_bar)).text("Triple Point").style("font-size", "12px").attr("fill", "black").attr("dominant-baseline", "middle");
svg.append("text").attr("x", x(Sci.T_c) + 10).attr("y", y(Sci.P_c_bar)).text("Critical Point").style("font-size", "12px").attr("fill", "black").attr("dominant-baseline", "middle");
const phaseText = svg.append("text").attr("x", (width + margin.left - margin.right) / 2).attr("y", (height + margin.top - margin.bottom) / 2).attr("text-anchor", "middle").attr("dominant-baseline", "middle").style("font-size", "4em").style("font-weight", "bold").style("fill", "rgba(0, 0, 0, 0.15)").style("pointer-events", "none");
const marker = svg.append("circle").attr("r", 6).attr("fill", "red").attr("stroke", "white").attr("stroke-width", 1.5);
// ==========================================================================
// 4. REACTIVITY
// ==========================================================================
let currentT = tempNum.valueAsNumber;
let currentP = pressureNum.valueAsNumber;
const PRESET_POINTS = {
ambient: { T: 298.15, P: 1.01325 },
sublimation: { T: 194.65, P: 1.01325 },
triple: { T: Sci.T_tp, P: Sci.P_tp_bar },
critical: { T: Sci.T_c, P: Sci.P_c_bar }
};
function setValues(T, P) { currentT = T; currentP = P; tempNum.value = T.toFixed(2); tempSlider.value = T; pressureNum.value = P.toPrecision(4); pressureSlider.value = Math.log(P); updateVisualization(); }
function updateVisualization() {
const phase = Sci.getPhase(currentT, currentP);
phaseText.text(null);
if (phase === 'Supercritical Fluid') { const xPos = (width + margin.left - margin.right) / 2; phaseText.append("tspan").attr("x", xPos).attr("dy", "-0.6em").text("Supercritical"); phaseText.append("tspan").attr("x", xPos).attr("dy", "1.2em").text("Fluid"); }
else if (phase === 'Critical Point') { const xPos = (width + margin.left - margin.right) / 2; phaseText.append("tspan").attr("x", xPos).attr("dy", "-0.6em").text("Critical"); phaseText.append("tspan").attr("x", xPos).attr("dy", "1.2em").text("Point"); }
else { phaseText.text(phase); }
marker.transition().duration(250).attr("cx", x(currentT)).attr("cy", y(currentP));
}
function debounce(func, delay) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; }
const debouncedUpdate = debounce(() => { currentT = tempNum.valueAsNumber; currentP = pressureNum.valueAsNumber; updateVisualization(); }, 50);
const plotOverlay = svg.append("rect").attr("x", margin.left).attr("y", margin.top).attr("width", width - margin.left - margin.right).attr("height", height - margin.top - margin.bottom).style("fill", "none").style("pointer-events", "all").style("cursor", "crosshair");
const tooltip = svg.append("text").attr("x", 0).attr("y", 0).style("font-size", "12px").style("pointer-events", "none").style("text-anchor", "middle").style("fill", "black").style("display", "none");
plotOverlay.on("mousemove", (event) => { const [mx, my] = d3.pointer(event); if (mx > margin.left && mx < width - margin.right && my > margin.top && my < height - margin.bottom) { const T = x.invert(mx); const P = y.invert(my); tooltip.style("display", "block").attr("x", mx).attr("y", my - 10).text(`T: ${T.toFixed(1)} K, P: ${P.toPrecision(3)} bar`); const distToVap = d3.least(vapLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); const distToSub = d3.least(subLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); const distToFus = d3.least(fusionLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); vapPath.classed("highlight", distToVap < 10); subPath.classed("highlight", distToSub < 10); fusPath.classed("highlight", distToFus < 10); } else { tooltip.style("display", "none"); } }).on("mouseleave", () => { tooltip.style("display", "none"); [vapPath, subPath, fusPath].forEach(p => p.classed("highlight", false)); }).on("click", (event) => { const [mx, my] = d3.pointer(event); const T = x.invert(mx); const P = y.invert(my); handleManualInput(); setValues(T, P); });
function handleManualInput() { presetButtons.forEach(b => b.classList.remove('active')); }
tempSlider.addEventListener('input', () => { tempNum.value = tempSlider.value; debouncedUpdate(); });
tempNum.addEventListener('input', () => { tempSlider.value = tempNum.value; debouncedUpdate(); });
pressureSlider.addEventListener('input', () => { const p_val = Math.exp(pressureSlider.valueAsNumber); pressureNum.value = p_val.toPrecision(4); debouncedUpdate(); });
pressureNum.addEventListener('input', () => { const p_val = pressureNum.valueAsNumber; if (p_val > 0) { pressureSlider.value = Math.log(p_val); } debouncedUpdate(); });
presetButtons.forEach(button => { button.addEventListener('click', () => { if (button.classList.contains('active')) { button.classList.remove('active'); } else { presetButtons.forEach(b => b.classList.remove('active')); button.classList.add('active'); const point = PRESET_POINTS[button.dataset.point]; setValues(point.T, point.P); } }); });
updateVisualization();
// ==========================================================================
// 5. RETURN THE FINAL WIDGET
// ==========================================================================
return widgetContainer;
}
Nitrogen, N2
{
// ==========================================================================
// 1. SCIENTIFIC HELPER MODULE FOR NITROGEN (N2)
// ==========================================================================
const Sci = (() => {
// --- Physical Constants for Nitrogen ---
const T_c = 126.192; const P_c_bar = 33.958; const T_tp = 63.151; const P_tp_bar = 0.12523;
function p_vaporization(T) { if (T < T_tp || T > T_c) return NaN; const theta = 1 - T / T_c; const n = [-6.0716454, 1.3074813, -1.2429833, -2.4841773]; const exponent = (T_c / T) * (n[0] * theta + n[1] * Math.pow(theta, 1.5) + n[2] * Math.pow(theta, 2.5) + n[3] * Math.pow(theta, 5)); return P_c_bar * Math.exp(exponent); }
function p_sublimation(T) { if (T >= T_tp) return NaN; const theta = 1 - T/T_tp; const T_ratio = T_tp / T; const n = [-13.04535, 1.295155]; const exponent = T_ratio * (n[0] * theta + n[1] * Math.pow(theta, 1.75)); return P_tp_bar * Math.exp(exponent); }
function t_fusion(P_bar) { if (P_bar < P_tp_bar) return NaN; const A = 2712.0, C = 1.75; return T_tp * Math.pow(((P_bar - P_tp_bar) / A) + 1, 1 / C); }
function getPhase(T, P_bar) { const isNearCriticalPoint = Math.abs(T - T_c) < 2.0 && Math.abs(P_bar - P_c_bar) < 2.0; if (isNearCriticalPoint) return 'Critical Point'; const isNearTriplePoint = Math.abs(T - T_tp) < 1.0 && Math.abs(Math.log10(P_bar) - Math.log10(P_tp_bar)) < 0.2; if (isNearTriplePoint) return 'Triple Point'; const t_fus = t_fusion(P_bar); if (!isNaN(t_fus) && T < t_fus) return 'Solid'; if (T < T_tp) { const p_sub = p_sublimation(T); return P_bar > p_sub ? 'Solid' : 'Gas'; } if (T > T_c && P_bar > P_c_bar) return 'Supercritical Fluid'; if (T > T_c) return 'Gas'; const p_vap = p_vaporization(T); if (!isNaN(p_vap)) { return P_bar > p_vap ? 'Liquid' : 'Gas'; } return 'Liquid'; }
return { T_c, P_c_bar, T_tp, P_tp_bar, getPhase, p_vaporization, p_sublimation, t_fusion };
})();
// ==========================================================================
// 2. WIDGET LAYOUT, STYLES, AND UI ELEMENT CREATION
// ==========================================================================
const vizSize = 600;
const t_min = 0, t_max = 1000;
const p_min = 0.001, p_max = 1000;
const log_p_min = Math.log(p_min), log_p_max = Math.log(p_max);
const uniqueId = `n2-phase-diagram-${crypto.randomUUID()}`;
const widgetContainer = html`
<style>
#${uniqueId} .control-group { display: flex; flex-direction: column; gap: 8px; }
#${uniqueId} .control-group label { font-weight: 500; font-size: 0.9em; color: #333; }
#${uniqueId} .control-group input[type=number] { width: 100%; box-sizing: border-box; border: none; border-bottom: 1.5px solid #ccc; border-radius: 0; background-color: transparent; padding: 8px 4px; font-size: 1em; transition: border-color 0.3s; }
#${uniqueId} .control-group input[type=number]:focus { border-color: #007bff; outline: none; }
#${uniqueId} .control-group input[type=range] { width: 100%; }
#${uniqueId} .presets { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
#${uniqueId} .preset-button { background: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; padding: 6px 12px; font-size: 0.9em; cursor: pointer; transition: background-color 0.2s, border-color 0.2s; }
#${uniqueId} .preset-button:hover { background-color: #e0e0e0; }
#${uniqueId} .preset-button.active { background-color: #007bff; color: white; border-color: #007bff; }
#${uniqueId} .boundary-line { fill: none; stroke: #007bff; stroke-width: 2.5; transition: stroke-width 0.2s; }
#${uniqueId} .boundary-line.highlight { stroke-width: 5; }
</style>
<div id="${uniqueId}" style="display: flex; flex-direction: column; gap: 20px; font-family: system-ui, sans-serif; max-width: 1100px; margin: auto;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; max-width: ${vizSize}px; margin: auto;">
<div class="control-group">
<label for="temp-num-${uniqueId}">Temperature (K)</label>
<input id="temp-num-${uniqueId}" type="number" min="${t_min}" max="${t_max}" step="0.1" value="298.15">
<input id="temp-slider-${uniqueId}" type="range" min="${t_min}" max="${t_max}" step="0.1" value="298.15">
</div>
<div class="control-group">
<label for="pressure-num-${uniqueId}">External Pressure (bar)</label>
<input id="pressure-num-${uniqueId}" type="number" min="${p_min}" max="${p_max}" step="any" value="1.01325">
<input id="pressure-slider-${uniqueId}" type="range" min="${log_p_min}" max="${log_p_max}" step="0.01" value="${Math.log(1.01325)}">
</div>
</div>
<div class="presets">
<button class="preset-button" data-point="ambient">Ambient</button>
<button class="preset-button" data-point="freezing">Normal Freezing</button>
<button class="preset-button" data-point="boiling">Normal Boiling</button>
<button class="preset-button" data-point="triple">Triple Pt</button>
<button class="preset-button" data-point="critical">Critical Pt</button>
</div>
<div style="display: flex; justify-content: center;">
<div id="plot-container-${uniqueId}"></div>
</div>
</div>`;
const tempNum = widgetContainer.querySelector(`#temp-num-${uniqueId}`);
const tempSlider = widgetContainer.querySelector(`#temp-slider-${uniqueId}`);
const pressureNum = widgetContainer.querySelector(`#pressure-num-${uniqueId}`);
const pressureSlider = widgetContainer.querySelector(`#pressure-slider-${uniqueId}`);
const presetButtons = widgetContainer.querySelectorAll(".preset-button");
const plotContainer = widgetContainer.querySelector(`#plot-container-${uniqueId}`);
// ==========================================================================
// 3. D3 PLOT SETUP
// ==========================================================================
const margin = {top: 50, right: 60, bottom: 50, left: 60};
const width = vizSize, height = vizSize;
const x = d3.scaleLinear().domain([t_min, t_max]).range([margin.left, width - margin.right]);
const y = d3.scaleLog().domain([p_min, p_max]).range([height - margin.bottom, margin.top]);
const svg = d3.create("svg").attr("width", width).attr("height", height).attr("viewBox", [0, 0, width, height]).style("max-width", "100%").style("height", "auto");
plotContainer.appendChild(svg.node());
const xAxisGroup = svg.append("g").attr("transform", `translate(0,${height - margin.bottom})`).call(d3.axisBottom(x));
xAxisGroup.select(".domain").remove();
xAxisGroup.selectAll(".tick line").clone().attr("y2", -height + margin.top + margin.bottom).attr("stroke-opacity", 0.1);
xAxisGroup.append("text").attr("x", width / 2).attr("y", margin.bottom - 10).attr("fill", "currentColor").attr("text-anchor", "middle").text("Temperature (K)");
const yAxisGroup = svg.append("g").attr("transform", `translate(${margin.left},0)`).call(d3.axisLeft(y).ticks(null, "~e"));
yAxisGroup.select(".domain").remove();
yAxisGroup.selectAll(".tick line").clone().attr("x2", width - margin.left - margin.right).attr("stroke-opacity", 0.1);
yAxisGroup.append("text").attr("transform", "rotate(-90)").attr("x", -height / 2).attr("y", -margin.left + 15).attr("fill", "currentColor").attr("text-anchor", "middle").text("Pressure (bar)");
const x_celsius = d3.scaleLinear().domain([t_min - 273.15, t_max - 273.15]).range(x.range());
const xAxisTopGroup = svg.append("g").attr("transform", `translate(0, ${margin.top})`).call(d3.axisTop(x_celsius));
xAxisTopGroup.select(".domain").remove();
xAxisTopGroup.append("text").attr("x", width / 2).attr("y", -margin.top + 15).attr("fill", "currentColor").attr("text-anchor", "middle").text("Temperature (°C)");
const y_pascal = d3.scaleLog().domain([p_min * 1e5, p_max * 1e5]).range(y.range());
const yAxisRightGroup = svg.append("g").attr("transform", `translate(${width - margin.right}, 0)`).call(d3.axisRight(y_pascal).ticks(null, "~e"));
yAxisRightGroup.select(".domain").remove();
yAxisRightGroup.append("text").attr("transform", "rotate(90)").attr("x", height / 2).attr("y", -margin.right + 20).attr("fill", "currentColor").attr("text-anchor", "middle").text("Pressure (Pa)");
const lineGen = d3.line().x(d => x(d[0])).y(d => y(d[1]));
const vapLineData = d3.range(Sci.T_tp, Sci.T_c, 0.5).map(t => [t, Sci.p_vaporization(t)]);
vapLineData.push([Sci.T_c, Sci.P_c_bar]);
const subLineData = d3.range(10, Sci.T_tp, 0.5).map(t => [t, Sci.p_sublimation(t)]).filter(d => d && d[1] >= p_min);
if (subLineData.length > 0) { subLineData.unshift([subLineData[0][0], p_min]); }
subLineData.push([Sci.T_tp, Sci.P_tp_bar]);
const fusionLineData = d3.range(Sci.P_tp_bar, p_max + 1, 1).map(p => [Sci.t_fusion(p), p]).filter(d => d && !isNaN(d[0]));
fusionLineData.unshift([Sci.T_tp, Sci.P_tp_bar]);
const clipPathId = `plot-area-clip-${uniqueId}`;
const clipPath = svg.append("defs").append("clipPath").attr("id", clipPathId).append("rect").attr("x", margin.left).attr("y", margin.top).attr("width", width - margin.left - margin.right).attr("height", height - margin.top - margin.bottom);
const shadedRegions = svg.append("g").attr("clip-path", `url(#${clipPathId})`);
// *** BUG FIX IS HERE: Corrected path definitions for all regions ***
const gasPath = [[t_max, p_min], [subLineData[0][0], p_min], ...subLineData.slice().reverse(), ...vapLineData, [t_max, Sci.P_c_bar]];
const solidPath = [[t_min, p_max], [fusionLineData[fusionLineData.length-1][0], p_max], ...fusionLineData.slice().reverse(), ...subLineData.reverse(), [t_min, p_min]];
const liquidPath = [...vapLineData.slice().reverse(), ...fusionLineData, [fusionLineData[fusionLineData.length-1][0], p_max], [Sci.T_c, p_max], [Sci.T_c, Sci.P_c_bar]];
const supercriticalPath = [[Sci.T_c, Sci.P_c_bar], [t_max, Sci.P_c_bar], [t_max, p_max], [Sci.T_c, p_max]];
shadedRegions.append("path").datum(gasPath).attr("d", lineGen).attr("fill", "rgba(200, 200, 200, 0.15)");
shadedRegions.append("path").datum(solidPath).attr("d", lineGen).attr("fill", "rgba(100, 100, 200, 0.1)");
shadedRegions.append("path").datum(liquidPath).attr("d", lineGen).attr("fill", "rgba(100, 200, 100, 0.1)");
shadedRegions.append("path").datum(supercriticalPath).attr("d", lineGen).attr("fill", "rgba(255, 165, 0, 0.15)");
const vapPath = svg.append("path").datum(vapLineData).attr("class", "boundary-line").attr("d", lineGen);
const subPath = svg.append("path").datum(subLineData).attr("class", "boundary-line").attr("d", lineGen);
const fusPath = svg.append("path").datum(fusionLineData).attr("class", "boundary-line").attr("d", lineGen);
svg.append("circle").attr("cx", x(Sci.T_tp)).attr("cy", y(Sci.P_tp_bar)).attr("r", 5).attr("fill", "black");
svg.append("circle").attr("cx", x(Sci.T_c)).attr("cy", y(Sci.P_c_bar)).attr("r", 5).attr("fill", "black");
svg.append("text").attr("x", x(Sci.T_tp) + 5).attr("y", y(Sci.P_tp_bar) + 15).text("Triple Point").style("font-size", "12px").attr("fill", "black");
svg.append("text").attr("x", x(Sci.T_c) + 5).attr("y", y(Sci.P_c_bar) - 5).text("Critical Point").style("font-size", "12px").attr("fill", "black");
const phaseText = svg.append("text").attr("x", (width + margin.left - margin.right) / 2).attr("y", (height + margin.top - margin.bottom) / 2).attr("text-anchor", "middle").attr("dominant-baseline", "middle").style("font-size", "4em").style("font-weight", "bold").style("fill", "rgba(0, 0, 0, 0.15)").style("pointer-events", "none");
const marker = svg.append("circle").attr("r", 6).attr("fill", "red").attr("stroke", "white").attr("stroke-width", 1.5);
// ==========================================================================
// 4. REACTIVITY
// ==========================================================================
let currentT = tempNum.valueAsNumber;
let currentP = pressureNum.valueAsNumber;
const PRESET_POINTS = {
ambient: { T: 298.15, P: 1.01325 },
freezing: { T: Sci.t_fusion(1.01325), P: 1.01325 },
boiling: { T: 77.35, P: 1.01325 },
triple: { T: Sci.T_tp, P: Sci.P_tp_bar },
critical: { T: Sci.T_c, P: Sci.P_c_bar }
};
function setValues(T, P) { currentT = T; currentP = P; tempNum.value = T.toFixed(2); tempSlider.value = T; pressureNum.value = P.toPrecision(4); pressureSlider.value = Math.log(P); updateVisualization(); }
function updateVisualization() {
const phase = Sci.getPhase(currentT, currentP);
phaseText.text(null);
if (phase === 'Supercritical Fluid') { const xPos = (width + margin.left - margin.right) / 2; phaseText.append("tspan").attr("x", xPos).attr("dy", "-0.6em").text("Supercritical"); phaseText.append("tspan").attr("x", xPos).attr("dy", "1.2em").text("Fluid"); }
else if (phase === 'Critical Point') { const xPos = (width + margin.left - margin.right) / 2; phaseText.append("tspan").attr("x", xPos).attr("dy", "-0.6em").text("Critical"); phaseText.append("tspan").attr("x", xPos).attr("dy", "1.2em").text("Point"); }
else { phaseText.text(phase); }
marker.transition().duration(250).attr("cx", x(currentT)).attr("cy", y(currentP));
}
function debounce(func, delay) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; }
const debouncedUpdate = debounce(() => { currentT = tempNum.valueAsNumber; currentP = pressureNum.valueAsNumber; updateVisualization(); }, 50);
const plotOverlay = svg.append("rect").attr("x", margin.left).attr("y", margin.top).attr("width", width - margin.left - margin.right).attr("height", height - margin.top - margin.bottom).style("fill", "none").style("pointer-events", "all").style("cursor", "crosshair");
const tooltip = svg.append("text").attr("x", 0).attr("y", 0).style("font-size", "12px").style("pointer-events", "none").style("text-anchor", "middle").style("fill", "black").style("display", "none");
plotOverlay.on("mousemove", (event) => { const [mx, my] = d3.pointer(event); if (mx > margin.left && mx < width - margin.right && my > margin.top && my < height - margin.bottom) { const T = x.invert(mx); const P = y.invert(my); tooltip.style("display", "block").attr("x", mx).attr("y", my - 10).text(`T: ${T.toFixed(1)} K, P: ${P.toPrecision(3)} bar`); const distToVap = d3.least(vapLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); const distToSub = d3.least(subLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); const distToFus = d3.least(fusionLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); vapPath.classed("highlight", distToVap < 10); subPath.classed("highlight", distToSub < 10); fusPath.classed("highlight", distToFus < 10); } else { tooltip.style("display", "none"); } }).on("mouseleave", () => { tooltip.style("display", "none"); [vapPath, subPath, fusPath].forEach(p => p.classed("highlight", false)); }).on("click", (event) => { const [mx, my] = d3.pointer(event); const T = x.invert(mx); const P = y.invert(my); handleManualInput(); setValues(T, P); });
function handleManualInput() { presetButtons.forEach(b => b.classList.remove('active')); }
tempSlider.addEventListener('input', () => { tempNum.value = tempSlider.value; debouncedUpdate(); });
tempNum.addEventListener('input', () => { tempSlider.value = tempNum.value; debouncedUpdate(); });
pressureSlider.addEventListener('input', () => { const p_val = Math.exp(pressureSlider.valueAsNumber); pressureNum.value = p_val.toPrecision(4); debouncedUpdate(); });
pressureNum.addEventListener('input', () => { const p_val = pressureNum.valueAsNumber; if (p_val > 0) { pressureSlider.value = Math.log(p_val); } debouncedUpdate(); });
presetButtons.forEach(button => { button.addEventListener('click', () => { if (button.classList.contains('active')) { button.classList.remove('active'); } else { presetButtons.forEach(b => b.classList.remove('active')); button.classList.add('active'); const point = PRESET_POINTS[button.dataset.point]; setValues(point.T, point.P); } }); });
updateVisualization();
// ==========================================================================
// 5. RETURN THE FINAL WIDGET
// ==========================================================================
return widgetContainer;
}
Oxygen, O2
{
// ==========================================================================
// 1. SCIENTIFIC HELPER MODULE FOR OXYGEN (O₂)
// ==========================================================================
const Sci = (() => {
// --- Physical Constants for Oxygen ---
const T_c = 154.581; const P_c_bar = 50.46; const T_tp = 54.361; const P_tp_bar = 0.00152;
function p_vaporization(T) { if (T < T_tp || T > T_c) return NaN; const theta = 1 - T / T_c; const n = [-6.03358, 1.16801, -0.11933, -1.0348, -1.2117]; const exponent = (T_c / T) * (n[0] * theta + n[1] * Math.pow(theta, 1.5) + n[2] * Math.pow(theta, 2) + n[3] * Math.pow(theta, 4) + n[4] * Math.pow(theta, 5)); return P_c_bar * Math.exp(exponent); }
function p_sublimation(T) { if (T >= T_tp) return NaN; const theta = T / T_tp; const n = [-11.1711, 1.4883]; const exponent = n[0] * (1/theta - 1) + n[1] * Math.log(theta); return P_tp_bar * Math.exp(exponent); }
function t_fusion(P_bar) { if (P_bar < P_tp_bar) return NaN; const A = 2595.0, C = 1.769; return T_tp * Math.pow(((P_bar - P_tp_bar) / A) + 1, 1 / C); }
function getPhase(T, P_bar) { const isNearCriticalPoint = Math.abs(T - T_c) < 3.0 && Math.abs(P_bar - P_c_bar) < 3.0; if (isNearCriticalPoint) return 'Critical Point'; const isNearTriplePoint = Math.abs(T - T_tp) < 1.0 && Math.abs(Math.log10(P_bar) - Math.log10(P_tp_bar)) < 0.3; if (isNearTriplePoint) return 'Triple Point'; const t_fus = t_fusion(P_bar); if (!isNaN(t_fus) && T < t_fus) return 'Solid'; if (T < T_tp) { const p_sub = p_sublimation(T); return P_bar > p_sub ? 'Solid' : 'Gas'; } if (T > T_c && P_bar > P_c_bar) return 'Supercritical Fluid'; if (T > T_c) return 'Gas'; const p_vap = p_vaporization(T); if (!isNaN(p_vap)) { return P_bar > p_vap ? 'Liquid' : 'Gas'; } return 'Liquid'; }
return { T_c, P_c_bar, T_tp, P_tp_bar, getPhase, p_vaporization, p_sublimation, t_fusion };
})();
// ==========================================================================
// 2. WIDGET LAYOUT, STYLES, AND UI ELEMENT CREATION
// ==========================================================================
const vizSize = 600;
const t_min = 0, t_max = 1000;
const p_min = 0.001, p_max = 1000;
const log_p_min = Math.log(p_min), log_p_max = Math.log(p_max);
const uniqueId = `o2-plot-only-${crypto.randomUUID()}`;
const widgetContainer = html`
<style>
#${uniqueId} .control-group { display: flex; flex-direction: column; gap: 8px; }
#${uniqueId} .control-group label { font-weight: 500; font-size: 0.9em; color: #333; }
#${uniqueId} .control-group input[type=number] { width: 100%; box-sizing: border-box; border: none; border-bottom: 1.5px solid #ccc; border-radius: 0; background-color: transparent; padding: 8px 4px; font-size: 1em; transition: border-color 0.3s; }
#${uniqueId} .control-group input[type=number]:focus { border-color: #007bff; outline: none; }
#${uniqueId} .control-group input[type=range] { width: 100%; }
#${uniqueId} .presets { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
#${uniqueId} .preset-button { background: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; padding: 6px 12px; font-size: 0.9em; cursor: pointer; transition: background-color 0.2s, border-color 0.2s; }
#${uniqueId} .preset-button:hover { background-color: #e0e0e0; }
#${uniqueId} .preset-button.active { background-color: #007bff; color: white; border-color: #007bff; }
#${uniqueId} .boundary-line { fill: none; stroke: #007bff; stroke-width: 2.5; transition: stroke-width 0.2s; }
#${uniqueId} .boundary-line.highlight { stroke-width: 5; }
</style>
<div id="${uniqueId}" style="display: flex; flex-direction: column; gap: 20px; font-family: system-ui, sans-serif; max-width: 1200px; margin: auto;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; max-width: ${vizSize}px; margin: auto;">
<div class="control-group">
<label for="temp-num-${uniqueId}">Temperature (K)</label>
<input id="temp-num-${uniqueId}" type="number" min="${t_min}" max="${t_max}" step="0.1" value="298.15">
<input id="temp-slider-${uniqueId}" type="range" min="${t_min}" max="${t_max}" step="0.1" value="298.15">
</div>
<div class="control-group">
<label for="pressure-num-${uniqueId}">External Pressure (bar)</label>
<input id="pressure-num-${uniqueId}" type="number" min="${p_min}" max="${p_max}" step="any" value="1.01325">
<input id="pressure-slider-${uniqueId}" type="range" min="${log_p_min}" max="${log_p_max}" step="0.01" value="${Math.log(1.01325)}">
</div>
</div>
<div class="presets">
<button class="preset-button" data-point="ambient">Ambient</button>
<button class="preset-button" data-point="freezing">Normal Freezing</button>
<button class="preset-button" data-point="boiling">Normal Boiling</button>
<button class="preset-button" data-point="triple">Triple Pt</button>
<button class="preset-button" data-point="critical">Critical Pt</button>
</div>
<div style="display: flex; justify-content: center;">
<div id="plot-container-${uniqueId}"></div>
</div>
</div>`;
const tempNum = widgetContainer.querySelector(`#temp-num-${uniqueId}`);
const tempSlider = widgetContainer.querySelector(`#temp-slider-${uniqueId}`);
const pressureNum = widgetContainer.querySelector(`#pressure-num-${uniqueId}`);
const pressureSlider = widgetContainer.querySelector(`#pressure-slider-${uniqueId}`);
const presetButtons = widgetContainer.querySelectorAll(".preset-button");
const plotContainer = widgetContainer.querySelector(`#plot-container-${uniqueId}`);
// ==========================================================================
// 3. D3 PLOT SETUP
// ==========================================================================
const margin = {top: 50, right: 60, bottom: 50, left: 60};
const width = vizSize, height = vizSize;
const x = d3.scaleLinear().domain([t_min, t_max]).range([margin.left, width - margin.right]);
const y = d3.scaleLog().domain([p_min, p_max]).range([height - margin.bottom, margin.top]);
const svg = d3.create("svg").attr("width", width).attr("height", height).attr("viewBox", [0, 0, width, height]).style("max-width", "100%").style("height", "auto");
plotContainer.appendChild(svg.node());
const xAxisGroup = svg.append("g").attr("transform", `translate(0,${height - margin.bottom})`).call(d3.axisBottom(x));
xAxisGroup.select(".domain").remove();
xAxisGroup.selectAll(".tick line").clone().attr("y2", -height + margin.top + margin.bottom).attr("stroke-opacity", 0.1);
xAxisGroup.append("text").attr("x", width / 2).attr("y", margin.bottom - 10).attr("fill", "currentColor").attr("text-anchor", "middle").text("Temperature (K)");
const yAxisGroup = svg.append("g").attr("transform", `translate(${margin.left},0)`).call(d3.axisLeft(y).ticks(null, "~e"));
yAxisGroup.select(".domain").remove();
yAxisGroup.selectAll(".tick line").clone().attr("x2", width - margin.left - margin.right).attr("stroke-opacity", 0.1);
yAxisGroup.append("text").attr("transform", "rotate(-90)").attr("x", -height / 2).attr("y", -margin.left + 15).attr("fill", "currentColor").attr("text-anchor", "middle").text("Pressure (bar)");
const x_celsius = d3.scaleLinear().domain([t_min - 273.15, t_max - 273.15]).range(x.range());
const xAxisTopGroup = svg.append("g").attr("transform", `translate(0, ${margin.top})`).call(d3.axisTop(x_celsius));
xAxisTopGroup.select(".domain").remove();
xAxisTopGroup.append("text").attr("x", width / 2).attr("y", -margin.top + 15).attr("fill", "currentColor").attr("text-anchor", "middle").text("Temperature (°C)");
const y_pascal = d3.scaleLog().domain([p_min * 1e5, p_max * 1e5]).range(y.range());
const yAxisRightGroup = svg.append("g").attr("transform", `translate(${width - margin.right}, 0)`).call(d3.axisRight(y_pascal).ticks(null, "~e"));
yAxisRightGroup.select(".domain").remove();
yAxisRightGroup.append("text").attr("transform", "rotate(90)").attr("x", height / 2).attr("y", -margin.right + 20).attr("fill", "currentColor").attr("text-anchor", "middle").text("Pressure (Pa)");
const lineGen = d3.line().x(d => x(d[0])).y(d => y(d[1]));
const vapLineData = d3.range(Sci.T_tp, Sci.T_c, 0.5).map(t => [t, Sci.p_vaporization(t)]);
vapLineData.push([Sci.T_c, Sci.P_c_bar]);
const subLineData = d3.range(t_min, Sci.T_tp, 0.5).map(t => [t, Sci.p_sublimation(t)]).filter(d => d && d[1] >= p_min);
if (subLineData.length > 0) {
subLineData.unshift([subLineData[0][0], p_min]);
}
subLineData.push([Sci.T_tp, Sci.P_tp_bar]);
const fusionLineData = d3.range(Sci.P_tp_bar, p_max + 1, 1).map(p => [Sci.t_fusion(p), p]).filter(d => d && !isNaN(d[0]));
fusionLineData.unshift([Sci.T_tp, Sci.P_tp_bar]);
const clipPathId = `plot-area-clip-${uniqueId}`;
const clipPath = svg.append("defs").append("clipPath").attr("id", clipPathId).append("rect").attr("x", margin.left).attr("y", margin.top).attr("width", width - margin.left - margin.right).attr("height", height - margin.top - margin.bottom);
const shadedRegions = svg.append("g").attr("clip-path", `url(#${clipPathId})`);
const fullAreaPath = [[t_min, p_min], [t_max, p_min], [t_max, p_max], [t_min, p_max]];
const solidPath = [
[t_min, p_min],
[t_min, p_max],
...[...fusionLineData].reverse(),
...[...subLineData].reverse()
];
const liquidPath = [...vapLineData, [Sci.T_c, p_max], ...fusionLineData.reverse()];
const supercriticalPath = [[Sci.T_c, Sci.P_c_bar], [t_max, Sci.P_c_bar], [t_max, p_max], [Sci.T_c, p_max]];
shadedRegions.append("path").datum(fullAreaPath).attr("d", lineGen).attr("fill", "rgba(200, 200, 200, 0.15)");
shadedRegions.append("path").datum(solidPath).attr("d", lineGen).attr("fill", "rgba(100, 100, 200, 0.1)");
shadedRegions.append("path").datum(liquidPath).attr("d", lineGen).attr("fill", "rgba(100, 200, 100, 0.1)");
shadedRegions.append("path").datum(supercriticalPath).attr("d", lineGen).attr("fill", "rgba(255, 165, 0, 0.15)");
const vapPath = svg.append("path").datum(vapLineData).attr("class", "boundary-line").attr("d", lineGen);
const subPath = svg.append("path").datum(subLineData).attr("class", "boundary-line").attr("d", lineGen);
const fusPath = svg.append("path").datum(fusionLineData).attr("class", "boundary-line").attr("d", lineGen);
svg.append("circle").attr("cx", x(Sci.T_tp)).attr("cy", y(Sci.P_tp_bar)).attr("r", 5).attr("fill", "black");
svg.append("circle").attr("cx", x(Sci.T_c)).attr("cy", y(Sci.P_c_bar)).attr("r", 5).attr("fill", "black");
svg.append("text").attr("x", x(Sci.T_tp) + 10).attr("y", y(Sci.P_tp_bar)).text("Triple Point").style("font-size", "12px").attr("fill", "black").attr("dominant-baseline", "middle");
svg.append("text").attr("x", x(Sci.T_c) + 10).attr("y", y(Sci.P_c_bar)).text("Critical Point").style("font-size", "12px").attr("fill", "black").attr("dominant-baseline", "middle");
const phaseText = svg.append("text").attr("x", (width + margin.left - margin.right) / 2).attr("y", (height + margin.top - margin.bottom) / 2).attr("text-anchor", "middle").attr("dominant-baseline", "middle").style("font-size", "4em").style("font-weight", "bold").style("fill", "rgba(0, 0, 0, 0.15)").style("pointer-events", "none");
const marker = svg.append("circle").attr("r", 6).attr("fill", "red").attr("stroke", "white").attr("stroke-width", 1.5);
// ==========================================================================
// 4. REACTIVITY
// ==========================================================================
let currentT = tempNum.valueAsNumber;
let currentP = pressureNum.valueAsNumber;
const PRESET_POINTS = {
ambient: { T: 298.15, P: 1.01325 },
freezing: { T: Sci.t_fusion(1.01325), P: 1.01325 },
boiling: { T: 90.188, P: 1.01325 },
triple: { T: Sci.T_tp, P: Sci.P_tp_bar },
critical: { T: Sci.T_c, P: Sci.P_c_bar }
};
function setValues(T, P) { currentT = T; currentP = P; tempNum.value = T.toFixed(2); tempSlider.value = T; pressureNum.value = P.toPrecision(4); pressureSlider.value = Math.log(P); updateVisualization(); }
function updateVisualization() { const phase = Sci.getPhase(currentT, currentP); phaseText.text(null); if (phase === 'Supercritical Fluid') { const xPos = (width + margin.left - margin.right) / 2; phaseText.append("tspan").attr("x", xPos).attr("dy", "-0.6em").text("Supercritical"); phaseText.append("tspan").attr("x", xPos).attr("dy", "1.2em").text("Fluid"); } else if (phase === 'Critical Point') { const xPos = (width + margin.left - margin.right) / 2; phaseText.append("tspan").attr("x", xPos).attr("dy", "-0.6em").text("Critical"); phaseText.append("tspan").attr("x", xPos).attr("dy", "1.2em").text("Point"); } else { phaseText.text(phase); } marker.transition().duration(250).attr("cx", x(currentT)).attr("cy", y(currentP)); }
function debounce(func, delay) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; }
const debouncedUpdate = debounce(() => { currentT = tempNum.valueAsNumber; currentP = pressureNum.valueAsNumber; updateVisualization(); }, 50);
const plotOverlay = svg.append("rect").attr("x", margin.left).attr("y", margin.top).attr("width", width - margin.left - margin.right).attr("height", height - margin.top - margin.bottom).style("fill", "none").style("pointer-events", "all").style("cursor", "crosshair");
const tooltip = svg.append("text").attr("x", 0).attr("y", 0).style("font-size", "12px").style("pointer-events", "none").style("text-anchor", "middle").style("fill", "black").style("display", "none");
plotOverlay.on("mousemove", (event) => { const [mx, my] = d3.pointer(event); if (mx > margin.left && mx < width - margin.right && my > margin.top && my < height - margin.bottom) { const T = x.invert(mx); const P = y.invert(my); tooltip.style("display", "block").attr("x", mx).attr("y", my - 10).text(`T: ${T.toFixed(1)} K, P: ${P.toPrecision(3)} bar`); const distToVap = d3.least(vapLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); const distToSub = d3.least(subLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); const distToFus = d3.least(fusionLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); vapPath.classed("highlight", distToVap < 10); subPath.classed("highlight", distToSub < 10); fusPath.classed("highlight", distToFus < 10); } else { tooltip.style("display", "none"); } }).on("mouseleave", () => { tooltip.style("display", "none"); [vapPath, subPath, fusPath].forEach(p => p.classed("highlight", false)); }).on("click", (event) => { const [mx, my] = d3.pointer(event); const T = x.invert(mx); const P = y.invert(my); handleManualInput(); setValues(T, P); });
function handleManualInput() { presetButtons.forEach(b => b.classList.remove('active')); }
tempSlider.addEventListener('input', () => { tempNum.value = tempSlider.value; debouncedUpdate(); });
tempNum.addEventListener('input', () => { tempSlider.value = tempNum.value; debouncedUpdate(); });
pressureSlider.addEventListener('input', () => { const p_val = Math.exp(pressureSlider.valueAsNumber); pressureNum.value = p_val.toPrecision(4); debouncedUpdate(); });
pressureNum.addEventListener('input', () => { const p_val = pressureNum.valueAsNumber; if (p_val > 0) { pressureSlider.value = Math.log(p_val); } debouncedUpdate(); });
presetButtons.forEach(button => { button.addEventListener('click', () => { if (button.classList.contains('active')) { button.classList.remove('active'); } else { presetButtons.forEach(b => b.classList.remove('active')); button.classList.add('active'); const point = PRESET_POINTS[button.dataset.point]; setValues(point.T, point.P); } }); });
updateVisualization();
// ==========================================================================
// 5. RETURN THE FINAL WIDGET
// ==========================================================================
return widgetContainer;
}
Hydrogen, H2
{
// ==========================================================================
// 1. SCIENTIFIC HELPER MODULE FOR HYDROGEN (H₂)
// ==========================================================================
const Sci = (() => {
// --- Physical Constants for Parahydrogen ---
const T_c = 32.938; const P_c_bar = 12.838; const T_tp = 13.8033; const P_tp_bar = 0.07042;
function p_vaporization(T) { if (T < T_tp || T > T_c) return NaN; const theta = 1 - T / T_c; const n = [-5.6567, 1.4746, -0.485, -0.322]; const exponent = (T_c / T) * (n[0] * theta + n[1] * Math.pow(theta, 1.5) + n[2] * Math.pow(theta, 2.5) + n[3] * Math.pow(theta, 5)); return P_c_bar * Math.exp(exponent); }
function p_sublimation(T) { if (T >= T_tp) return NaN; const theta = T / T_tp; const n = [-10.51, 2.56]; const exponent = n[0] * (1/theta - 1) + n[1] * Math.log(theta); return P_tp_bar * Math.exp(exponent); }
function t_fusion(P_bar) { if (P_bar < P_tp_bar) return NaN; const A = 274.0, C = 1.74; return T_tp * Math.pow(((P_bar - P_tp_bar) / A) + 1, 1 / C); }
function getPhase(T, P_bar) { const isNearCriticalPoint = Math.abs(T - T_c) < 1.0 && Math.abs(P_bar - P_c_bar) < 1.0; if (isNearCriticalPoint) return 'Critical Point'; const isNearTriplePoint = Math.abs(T - T_tp) < 0.5 && Math.abs(Math.log10(P_bar) - Math.log10(P_tp_bar)) < 0.2; if (isNearTriplePoint) return 'Triple Point'; const t_fus = t_fusion(P_bar); if (!isNaN(t_fus) && T < t_fus) return 'Solid'; if (T < T_tp) { const p_sub = p_sublimation(T); return P_bar > p_sub ? 'Solid' : 'Gas'; } if (T > T_c && P_bar > P_c_bar) return 'Supercritical Fluid'; if (T > T_c) return 'Gas'; const p_vap = p_vaporization(T); if (!isNaN(p_vap)) { return P_bar > p_vap ? 'Liquid' : 'Gas'; } return 'Liquid'; }
return { T_c, P_c_bar, T_tp, P_tp_bar, getPhase, p_vaporization, p_sublimation, t_fusion };
})();
// ==========================================================================
// 2. WIDGET LAYOUT, STYLES, AND UI ELEMENT CREATION
// ==========================================================================
const vizSize = 600;
const t_min = 0, t_max = 1000;
const p_min = 0.001, p_max = 1000;
const log_p_min = Math.log(p_min), log_p_max = Math.log(p_max);
const uniqueId = `h2-plot-only-${crypto.randomUUID()}`;
const widgetContainer = html`
<style>
#${uniqueId} .control-group { display: flex; flex-direction: column; gap: 8px; }
#${uniqueId} .control-group label { font-weight: 500; font-size: 0.9em; color: #333; }
#${uniqueId} .control-group input[type=number] { width: 100%; box-sizing: border-box; border: none; border-bottom: 1.5px solid #ccc; border-radius: 0; background-color: transparent; padding: 8px 4px; font-size: 1em; transition: border-color 0.3s; }
#${uniqueId} .control-group input[type=number]:focus { border-color: #007bff; outline: none; }
#${uniqueId} .control-group input[type=range] { width: 100%; }
#${uniqueId} .presets { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
#${uniqueId} .preset-button { background: #f0f0f0; border: 1px solid #ccc; border-radius: 4px; padding: 6px 12px; font-size: 0.9em; cursor: pointer; transition: background-color 0.2s, border-color 0.2s; }
#${uniqueId} .preset-button:hover { background-color: #e0e0e0; }
#${uniqueId} .preset-button.active { background-color: #007bff; color: white; border-color: #007bff; }
#${uniqueId} .boundary-line { fill: none; stroke: #007bff; stroke-width: 2.5; transition: stroke-width 0.2s; }
#${uniqueId} .boundary-line.highlight { stroke-width: 5; }
</style>
<div id="${uniqueId}" style="display: flex; flex-direction: column; gap: 20px; font-family: system-ui, sans-serif; max-width: 1200px; margin: auto;">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px; max-width: ${vizSize}px; margin: auto;">
<div class="control-group">
<label for="temp-num-${uniqueId}">Temperature (K)</label>
<input id="temp-num-${uniqueId}" type="number" min="${t_min}" max="${t_max}" step="0.1" value="298.15">
<input id="temp-slider-${uniqueId}" type="range" min="${t_min}" max="${t_max}" step="0.1" value="298.15">
</div>
<div class="control-group">
<label for="pressure-num-${uniqueId}">External Pressure (bar)</label>
<input id="pressure-num-${uniqueId}" type="number" min="${p_min}" max="${p_max}" step="any" value="1.01325">
<input id="pressure-slider-${uniqueId}" type="range" min="${log_p_min}" max="${log_p_max}" step="0.01" value="${Math.log(1.01325)}">
</div>
</div>
<div class="presets">
<button class="preset-button" data-point="ambient">Ambient</button>
<button class="preset-button" data-point="boiling">Normal Boiling</button>
<button class="preset-button" data-point="triple">Triple Pt</button>
<button class="preset-button" data-point="critical">Critical Pt</button>
</div>
<div style="display: flex; justify-content: center;">
<div id="plot-container-${uniqueId}"></div>
</div>
</div>`;
const tempNum = widgetContainer.querySelector(`#temp-num-${uniqueId}`);
const tempSlider = widgetContainer.querySelector(`#temp-slider-${uniqueId}`);
const pressureNum = widgetContainer.querySelector(`#pressure-num-${uniqueId}`);
const pressureSlider = widgetContainer.querySelector(`#pressure-slider-${uniqueId}`);
const presetButtons = widgetContainer.querySelectorAll(".preset-button");
const plotContainer = widgetContainer.querySelector(`#plot-container-${uniqueId}`);
// ==========================================================================
// 3. D3 PLOT SETUP
// ==========================================================================
const margin = {top: 50, right: 60, bottom: 50, left: 60};
const width = vizSize, height = vizSize;
const x = d3.scaleLinear().domain([t_min, t_max]).range([margin.left, width - margin.right]);
const y = d3.scaleLog().domain([p_min, p_max]).range([height - margin.bottom, margin.top]);
const svg = d3.create("svg").attr("width", width).attr("height", height).attr("viewBox", [0, 0, width, height]).style("max-width", "100%").style("height", "auto");
plotContainer.appendChild(svg.node());
const xAxisGroup = svg.append("g").attr("transform", `translate(0,${height - margin.bottom})`).call(d3.axisBottom(x));
xAxisGroup.select(".domain").remove();
xAxisGroup.selectAll(".tick line").clone().attr("y2", -height + margin.top + margin.bottom).attr("stroke-opacity", 0.1);
xAxisGroup.append("text").attr("x", width / 2).attr("y", margin.bottom - 10).attr("fill", "currentColor").attr("text-anchor", "middle").text("Temperature (K)");
const yAxisGroup = svg.append("g").attr("transform", `translate(${margin.left},0)`).call(d3.axisLeft(y).ticks(null, "~e"));
yAxisGroup.select(".domain").remove();
yAxisGroup.selectAll(".tick line").clone().attr("x2", width - margin.left - margin.right).attr("stroke-opacity", 0.1);
yAxisGroup.append("text").attr("transform", "rotate(-90)").attr("x", -height / 2).attr("y", -margin.left + 15).attr("fill", "currentColor").attr("text-anchor", "middle").text("Pressure (bar)");
const x_celsius = d3.scaleLinear().domain([t_min - 273.15, t_max - 273.15]).range(x.range());
const xAxisTopGroup = svg.append("g").attr("transform", `translate(0, ${margin.top})`).call(d3.axisTop(x_celsius));
xAxisTopGroup.select(".domain").remove();
xAxisTopGroup.append("text").attr("x", width / 2).attr("y", -margin.top + 15).attr("fill", "currentColor").attr("text-anchor", "middle").text("Temperature (°C)");
const y_pascal = d3.scaleLog().domain([p_min * 1e5, p_max * 1e5]).range(y.range());
const yAxisRightGroup = svg.append("g").attr("transform", `translate(${width - margin.right}, 0)`).call(d3.axisRight(y_pascal).ticks(null, "~e"));
yAxisRightGroup.select(".domain").remove();
yAxisRightGroup.append("text").attr("transform", "rotate(90)").attr("x", height / 2).attr("y", -margin.right + 20).attr("fill", "currentColor").attr("text-anchor", "middle").text("Pressure (Pa)");
const lineGen = d3.line().x(d => x(d[0])).y(d => y(d[1]));
const vapLineData = d3.range(Sci.T_tp, Sci.T_c, 0.1).map(t => [t, Sci.p_vaporization(t)]);
vapLineData.push([Sci.T_c, Sci.P_c_bar]);
const subLineData = d3.range(5, Sci.T_tp, 0.1).map(t => [t, Sci.p_sublimation(t)]).filter(d => d && d[1] >= p_min);
const fusionLineData = d3.range(Sci.P_tp_bar, p_max + 1, 1).map(p => [Sci.t_fusion(p), p]).filter(d => d && !isNaN(d[0]));
fusionLineData.unshift([Sci.T_tp, Sci.P_tp_bar]);
const clipPathId = `plot-area-clip-${uniqueId}`;
const clipPath = svg.append("defs").append("clipPath").attr("id", clipPathId).append("rect").attr("x", margin.left).attr("y", margin.top).attr("width", width - margin.left - margin.right).attr("height", height - margin.top - margin.bottom);
const shadedRegions = svg.append("g").attr("clip-path", `url(#${clipPathId})`);
const fullAreaPath = [[t_min, p_min], [t_max, p_min], [t_max, p_max], [t_min, p_max]];
const solidPath = [...subLineData, ...fusionLineData, [t_max, p_max], [t_min, p_max]];
const liquidPath = [...vapLineData.slice().reverse(), ...fusionLineData.filter(d => d[0] <= Sci.T_c)];
const supercriticalPath = [[Sci.T_c, Sci.P_c_bar], [t_max, Sci.P_c_bar], [t_max, p_max], [Sci.T_c, p_max]];
shadedRegions.append("path").datum(fullAreaPath).attr("d", lineGen).attr("fill", "rgba(200, 200, 200, 0.15)");
shadedRegions.append("path").datum(solidPath).attr("d", lineGen).attr("fill", "rgba(100, 100, 200, 0.1)");
shadedRegions.append("path").datum(liquidPath).attr("d", lineGen).attr("fill", "rgba(100, 200, 100, 0.1)");
shadedRegions.append("path").datum(supercriticalPath).attr("d", lineGen).attr("fill", "rgba(255, 165, 0, 0.15)");
const vapPath = svg.append("path").datum(vapLineData).attr("class", "boundary-line").attr("d", lineGen);
const subPath = svg.append("path").datum(subLineData).attr("class", "boundary-line").attr("d", lineGen);
const fusPath = svg.append("path").datum(fusionLineData).attr("class", "boundary-line").attr("d", lineGen);
svg.append("circle").attr("cx", x(Sci.T_tp)).attr("cy", y(Sci.P_tp_bar)).attr("r", 5).attr("fill", "black");
svg.append("circle").attr("cx", x(Sci.T_c)).attr("cy", y(Sci.P_c_bar)).attr("r", 5).attr("fill", "black");
svg.append("text").attr("x", x(Sci.T_tp) + 10).attr("y", y(Sci.P_tp_bar) ).text("Triple Point").style("font-size", "12px").attr("fill", "black").attr("dominant-baseline", "middle");
svg.append("text").attr("x", x(Sci.T_c) + 10).attr("y", y(Sci.P_c_bar)).text("Critical Point").style("font-size", "12px").attr("fill", "black").attr("dominant-baseline", "middle");
const phaseText = svg.append("text").attr("x", (width + margin.left - margin.right) / 2).attr("y", (height + margin.top - margin.bottom) / 2).attr("text-anchor", "middle").attr("dominant-baseline", "middle").style("font-size", "4em").style("font-weight", "bold").style("fill", "rgba(0, 0, 0, 0.15)").style("pointer-events", "none");
const marker = svg.append("circle").attr("r", 6).attr("fill", "red").attr("stroke", "white").attr("stroke-width", 1.5);
// ==========================================================================
// 4. REACTIVITY
// ==========================================================================
let currentT = tempNum.valueAsNumber;
let currentP = pressureNum.valueAsNumber;
const PRESET_POINTS = {
ambient: { T: 298.15, P: 1.01325 },
boiling: { T: 20.27, P: 1.01325 },
triple: { T: Sci.T_tp, P: Sci.P_tp_bar },
critical: { T: Sci.T_c, P: Sci.P_c_bar }
};
function setValues(T, P) { currentT = T; currentP = P; tempNum.value = T.toFixed(2); tempSlider.value = T; pressureNum.value = P.toPrecision(4); pressureSlider.value = Math.log(P); updateVisualization(); }
function updateVisualization() { const phase = Sci.getPhase(currentT, currentP); phaseText.text(null); if (phase === 'Supercritical Fluid') { const xPos = (width + margin.left - margin.right) / 2; phaseText.append("tspan").attr("x", xPos).attr("dy", "-0.6em").text("Supercritical"); phaseText.append("tspan").attr("x", xPos).attr("dy", "1.2em").text("Fluid"); } else if (phase === 'Critical Point') { const xPos = (width + margin.left - margin.right) / 2; phaseText.append("tspan").attr("x", xPos).attr("dy", "-0.6em").text("Critical"); phaseText.append("tspan").attr("x", xPos).attr("dy", "1.2em").text("Point"); } else { phaseText.text(phase); } marker.transition().duration(250).attr("cx", x(currentT)).attr("cy", y(currentP)); }
function debounce(func, delay) { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), delay); }; }
const debouncedUpdate = debounce(() => { currentT = tempNum.valueAsNumber; currentP = pressureNum.valueAsNumber; updateVisualization(); }, 50);
const plotOverlay = svg.append("rect").attr("x", margin.left).attr("y", margin.top).attr("width", width - margin.left - margin.right).attr("height", height - margin.top - margin.bottom).style("fill", "none").style("pointer-events", "all").style("cursor", "crosshair");
const tooltip = svg.append("text").attr("x", 0).attr("y", 0).style("font-size", "12px").style("pointer-events", "none").style("text-anchor", "middle").style("fill", "black").style("display", "none");
plotOverlay.on("mousemove", (event) => { const [mx, my] = d3.pointer(event); if (mx > margin.left && mx < width - margin.right && my > margin.top && my < height - margin.bottom) { const T = x.invert(mx); const P = y.invert(my); tooltip.style("display", "block").attr("x", mx).attr("y", my - 10).text(`T: ${T.toFixed(1)} K, P: ${P.toPrecision(3)} bar`); const distToVap = d3.least(vapLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); const distToSub = d3.least(subLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); const distToFus = d3.least(fusionLineData, d => Math.hypot(x(d[0])-mx, y(d[1])-my)); vapPath.classed("highlight", distToVap < 10); subPath.classed("highlight", distToSub < 10); fusPath.classed("highlight", distToFus < 10); } else { tooltip.style("display", "none"); } }).on("mouseleave", () => { tooltip.style("display", "none"); [vapPath, subPath, fusPath].forEach(p => p.classed("highlight", false)); }).on("click", (event) => { const [mx, my] = d3.pointer(event); const T = x.invert(mx); const P = y.invert(my); handleManualInput(); setValues(T, P); });
function handleManualInput() { presetButtons.forEach(b => b.classList.remove('active')); }
tempSlider.addEventListener('input', () => { tempNum.value = tempSlider.value; debouncedUpdate(); });
tempNum.addEventListener('input', () => { tempSlider.value = tempNum.value; debouncedUpdate(); });
pressureSlider.addEventListener('input', () => { const p_val = Math.exp(pressureSlider.valueAsNumber); pressureNum.value = p_val.toPrecision(4); debouncedUpdate(); });
pressureNum.addEventListener('input', () => { const p_val = pressureNum.valueAsNumber; if (p_val > 0) { pressureSlider.value = Math.log(p_val); } debouncedUpdate(); });
presetButtons.forEach(button => { button.addEventListener('click', () => { if (button.classList.contains('active')) { button.classList.remove('active'); } else { presetButtons.forEach(b => b.classList.remove('active')); button.classList.add('active'); const point = PRESET_POINTS[button.dataset.point]; setValues(point.T, point.P); } }); });
updateVisualization();
// ==========================================================================
// 5. RETURN THE FINAL WIDGET
// ==========================================================================
return widgetContainer;
}