Protein Folding Pathway Visualizer Script

Protein Folding Pathway Visualizer

Folding Pathway Graph

Go to the "Pathway Configuration" tab to define states and transitions, then click "Generate Visualization."

1. Define Folding States (Nodes)

Current States

2. Define Transitions (Links)

Current Transitions

No states added yet. Start with 'U' (Unfolded) and 'N' (Native).

"; } else { states.forEach(s => { const itemEl = document.createElement("div"); itemEl.className = "pfv-config-list-item"; itemEl.dataset.id = s.id; itemEl.innerHTML = ` ${escapeHTML(s.id)} - ${escapeHTML(s.stability)} (Energy: ${s.energy}) `; statesList.appendChild(itemEl); }); } // --- Links List --- linksList.innerHTML = ""; if (links.length === 0) { linksList.innerHTML = "

No transitions added yet.

"; } else { links.forEach(l => { const itemEl = document.createElement("div"); itemEl.className = "pfv-config-list-item"; itemEl.dataset.id = `${l.source}-${l.target}`; itemEl.innerHTML = ` ${escapeHTML(l.source)} → ${escapeHTML(l.target)} (Rate: ${l.rate}) `; linksList.appendChild(itemEl); }); } updateLinkDropdowns(); } /** * Handles adding a new state */ function handleAddState(e) { e.preventDefault(); const id = stateIdInput.value.trim().toUpperCase(); const energy = parseFloat(stateEnergyInput.value); const stability = stateStabilityInput.value.trim(); if (states.find(s => s.id === id)) { alert(`State ID "${id}" already exists.`); return; } const newState = { id, energy, stability }; states.push(newState); addStateForm.reset(); updateConfigLists(); // Default to a low energy value next time stateEnergyInput.value = Math.max(1, energy - 1); } /** * Handles adding a new transition (link) */ function handleAddLink(e) { e.preventDefault(); const source = linkSourceSelect.value; const target = linkTargetSelect.value; const rate = parseFloat(linkRateInput.value); if (!source || !target) { alert("Please select both source and target states."); return; } if (source === target) { alert("Source and Target states must be different."); return; } if (links.some(l => l.source === source && l.target === target)) { alert(`Transition already exists from ${source} to ${target}.`); return; } const newLink = { source, target, rate }; links.push(newLink); // Reset link inputs except for rate linkSourceSelect.value = ""; linkTargetSelect.value = ""; updateConfigLists(); } /** * Handles removing states or links from the config lists */ function handleConfigItemRemove(e) { const itemEl = e.target.closest(".pfv-config-list-item"); if (!itemEl) return; const id = itemEl.dataset.id; if (e.target.dataset.action === "remove-state") { if (links.some(l => l.source === id || l.target === id)) { alert(`Cannot remove state "${id}". It is linked to existing transitions. Remove links first.`); return; } states = states.filter(s => s.id !== id); } else if (e.target.dataset.action === "remove-link") { const [source, target] = id.split('-'); links = links.filter(l => !(l.source === source && l.target === target)); } updateConfigLists(); } /** * Renders the D3 Force-Directed Graph */ function renderVisualization() { if (states.length === 0) { vizContainer.innerHTML = ''; vizEmptyMsg.style.display = 'block'; return; } vizEmptyMsg.style.display = 'none'; // Clear previous SVG d3.select(vizContainer).select("svg").remove(); const svg = d3.select(vizContainer).append("svg") .attr("width", VIZ_WIDTH) .attr("height", VIZ_HEIGHT); // Define arrowhead marker svg.append('defs').append('marker') .attr('id', 'arrowhead') .attr('viewBox', '-0 -5 10 10') .attr('refX', 15) // Position marker relative to circle radius .attr('refY', 0) .attr('orient', 'auto') .attr('markerWidth', 5) .attr('markerHeight', 5) .attr('xoverflow', 'visible') .append('svg:path') .attr('d', 'M 0, -5 L 10 ,0 L 0, 5') .attr('fill', '#999') .style('stroke', 'none'); // Create a deep copy of nodes/links for the simulation (D3 mutates them) const nodes = states.map(d => ({ ...d })); const currentLinks = links.map(d => ({ ...d })); const simulation = d3.forceSimulation(nodes) .force("link", d3.forceLink(currentLinks).id(d => d.id).distance(150)) .force("charge", d3.forceManyBody().strength(-300)) .force("center", d3.forceCenter(VIZ_WIDTH / 2, VIZ_HEIGHT / 2)) .force("x", d3.forceX(VIZ_WIDTH / 2).strength(0.05)) .force("y", d3.forceY(VIZ_HEIGHT / 2).strength(0.05)); // Draw links const link = svg.append("g") .attr("class", "links") .selectAll("line") .data(currentLinks) .enter().append("line") .attr("class", "pfv-link") .attr("stroke-width", d => Math.sqrt(d.rate) * 3) // Thicker line for higher rate .style("stroke-dasharray", d => d.rate < 0.5 ? "5, 5" : "none") // Dashed for low rate/high barrier .attr("stroke", d => d3.select("html").style("--pfv-link-rate-color", STATE_COLOR_SCALE(d.rate))); // Base stroke color // Draw nodes (as groups to include text) const node = svg.append("g") .attr("class", "nodes") .selectAll(".pfv-node") .data(nodes) .enter().append("g") .attr("class", "pfv-node") .call(d3.drag() .on("start", dragstarted) .on("drag", dragged) .on("end", dragended)); // Node circle (size by stability, color by energy) node.append("circle") .attr("r", d => STATE_RADIUS_SCALE(d.energy)) .attr("fill", d => STATE_COLOR_SCALE(d.energy)); // Node ID text (center) node.append("text") .attr("dy", 4) .attr("text-anchor", "middle") .attr("class", "node-name") .text(d => d.id); // Node stability text (below) node.append("text") .attr("dy", d => STATE_RADIUS_SCALE(d.energy) + 12) .attr("text-anchor", "middle") .text(d => d.stability); // Ticker function simulation.on("tick", () => { link .attr("x1", d => d.source.x) .attr("y1", d => d.source.y) .attr("x2", d => d.target.x) .attr("y2", d => d.target.y); node .attr("transform", d => `translate(${d.x},${d.y})`); }); // Drag functions function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event, d) { d.fx = event.x; d.fy = event.y; } function dragended(event, d) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } } /** * Generates a PDF report from the pathway data */ function downloadPDF() { if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF.autoTable === 'undefined') { alert("Error: PDF libraries could not be loaded. Please try again."); return; } if (states.length === 0) { alert("Please add states and transitions first."); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF("p", "pt", "a4"); const margin = 40; let yPos = margin; doc.setFontSize(18); doc.text("Protein Folding Pathway Data", margin, yPos); yPos += 30; const tableOptions = { startY: yPos, theme: 'striped', headStyles: { fillColor: [0, 115, 230], textColor: [255, 255, 255] }, margin: { left: margin, right: margin } }; // --- 1. States Table --- const stateBody = states.map(s => [s.id, s.energy, s.stability]); doc.setFontSize(14); doc.text("Folding States (Nodes)", margin, yPos); yPos += 20; tableOptions.startY = yPos; tableOptions.head = [["State ID", "Relative Energy (1-10)", "Stability/Conformation"]]; tableOptions.body = stateBody; doc.autoTable(tableOptions); yPos = doc.autoTable.previous.finalY + 30; // --- 2. Transitions Table --- const linkBody = links.map(l => [ l.source, l.target, l.rate.toFixed(2), l.rate >= 1 ? 'High' : 'Low' ]); // Check for page break before adding next table if (yPos > doc.internal.pageSize.getHeight() - 100) { doc.addPage(); yPos = margin; } doc.setFontSize(14); doc.text("Transitions (Links)", margin, yPos); yPos += 20; tableOptions.startY = yPos; tableOptions.head = [["Source State", "Target State", "Rate/Barrier Value", "Transition Speed"]]; tableOptions.body = linkBody; doc.autoTable(tableOptions); doc.save("Protein_Folding_Pathway_Data.pdf"); } /** * Helper to escape HTML */ function escapeHTML(str) { if (!str) return ""; return str .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } // --- 3. INITIALIZATION & EVENT LISTENERS --- // Tab Listeners tabButtons.forEach((btn) => { btn.addEventListener("click", () => showTab(btn.dataset.target)); }); navButtons.forEach((btn) => { btn.addEventListener("click", () => showTab(btn.dataset.target)); }); // Config Tab Listeners addStateForm.addEventListener("submit", handleAddState); addLinkForm.addEventListener("submit", handleAddLink); statesList.addEventListener("click", handleConfigItemRemove); linksList.addEventListener("click", handleConfigItemRemove); if (generateBtn) { generateBtn.addEventListener("click", () => renderVisualization() || showTab('pfv-tab-dashboard')); } // Dashboard Tab Listeners if (pdfBtn) { pdfBtn.addEventListener("click", downloadPDF); } // Set initial size of visualization container vizContainer.style.width = '100%'; vizContainer.style.height = `${VIZ_HEIGHT}px`; // Load sample data states = [ { id: 'U', energy: 10, stability: 'Unfolded/Denatured' }, { id: 'I', energy: 5, stability: 'Intermediate/Molten Globule' }, { id: 'N', energy: 1, stability: 'Native/Folded' }, { id: 'A', energy: 8, stability: 'Aggregation State' } ]; links = [ { source: 'U', target: 'I', rate: 0.8 }, { source: 'I', target: 'N', rate: 1.5 }, { source: 'U', target: 'N', rate: 0.1 }, // Slow, high barrier path { source: 'U', target: 'A', rate: 0.3 } ]; updateConfigLists(); // Initial State showTab("pfv-tab-dashboard"); });
Scroll to Top