// Educational browser model source snapshot. Not the native oNeura engine or a validated clinical model. // Generated from the inline script in ddi.html. No patient decisions. // ─── DRUG DATABASE ─── const DRUGS = [ { id:'warfarin', name:'Warfarin', mw:308, logp:3.0, proteinBinding:0.99, cypSubstrates:['CYP2C9'], cypInhibitors:[], pgpSubstrate:false, pgpInhibitor:false, halfLife:40, tmax:4, bioavailability:0.80, therapeuticIndex:'narrow', // narrow TI organEffects:['Anticoagulation','Bleeding risk'], clearance:0.17, // L/h/kg scaled concept dose_mg:5, color:'#c25a3f', // PD: effect on INR / coagulation receptorAffinity:0.95, // VKORC1 inhibition affectedSystems:['coagulation'] }, { id:'fluconazole', name:'Fluconazole', mw:306, logp:0.5, proteinBinding:0.12, cypSubstrates:[], cypInhibitors:['CYP2C9','CYP3A4'], pgpSubstrate:false, pgpInhibitor:false, halfLife:30, tmax:2, bioavailability:0.90, therapeuticIndex:'wide', organEffects:['Antifungal','Hepatotoxicity'], clearance:0.23, dose_mg:200, color:'#b78c63', receptorAffinity:0.7, affectedSystems:['fungal'] }, { id:'phenytoin', name:'Phenytoin', mw:252, logp:2.5, proteinBinding:0.90, cypSubstrates:['CYP2C9'], cypInhibitors:[], pgpSubstrate:false, pgpInhibitor:false, halfLife:22, tmax:6, bioavailability:0.85, therapeuticIndex:'narrow', organEffects:['Seizure control','CNS depression','Gingival hyperplasia'], clearance:0.18, dose_mg:300, color:'#8a9b5e', receptorAffinity:0.85, // Na+ channel block affectedSystems:['cns'] }, { id:'metoprolol', name:'Metoprolol', mw:267, logp:1.9, proteinBinding:0.12, cypSubstrates:['CYP2D6'], cypInhibitors:[], pgpSubstrate:false, pgpInhibitor:false, halfLife:3.5, tmax:1.5, bioavailability:0.50, therapeuticIndex:'moderate', organEffects:['Heart rate reduction','BP lowering','Bronchospasm'], clearance:0.75, dose_mg:100, color:'#a8784a', receptorAffinity:0.80, // beta-1 blockade affectedSystems:['cardiovascular'] }, { id:'amiodarone', name:'Amiodarone', mw:645, logp:7.6, proteinBinding:0.96, cypSubstrates:[], cypInhibitors:['CYP3A4','CYP2C9'], pgpSubstrate:false, pgpInhibitor:true, halfLife:55, tmax:5, bioavailability:0.45, therapeuticIndex:'narrow', organEffects:['QT prolongation','Thyroid dysfunction','Pulmonary toxicity'], clearance:0.12, dose_mg:200, color:'#c2603f', receptorAffinity:0.90, // multi-channel block affectedSystems:['cardiovascular','thyroid'] }, { id:'simvastatin', name:'Simvastatin', mw:419, logp:4.7, proteinBinding:0.95, cypSubstrates:['CYP3A4'], cypInhibitors:[], pgpSubstrate:false, pgpInhibitor:false, halfLife:2.8, tmax:1.5, bioavailability:0.05, therapeuticIndex:'moderate', organEffects:['Cholesterol reduction','Myopathy','Rhabdomyolysis'], clearance:0.85, dose_mg:40, color:'#e09a4f', receptorAffinity:0.75, // HMG-CoA reductase affectedSystems:['musculoskeletal','hepatic'] }, { id:'clarithromycin', name:'Clarithromycin', mw:748, logp:3.2, proteinBinding:0.72, cypSubstrates:[], cypInhibitors:['CYP3A4'], pgpSubstrate:false, pgpInhibitor:true, halfLife:5, tmax:2.5, bioavailability:0.55, therapeuticIndex:'wide', organEffects:['Antibacterial','QT prolongation','GI disturbance'], clearance:0.50, dose_mg:500, color:'#ad6155', receptorAffinity:0.6, affectedSystems:['cardiovascular','gi'] }, { id:'digoxin', name:'Digoxin', mw:781, logp:1.3, proteinBinding:0.25, cypSubstrates:[], cypInhibitors:[], pgpSubstrate:true, pgpInhibitor:false, halfLife:40, tmax:2, bioavailability:0.70, therapeuticIndex:'narrow', organEffects:['Inotropic effect','AV node block','Arrhythmia risk'], clearance:0.088, dose_mg:0.25, color:'#826853', receptorAffinity:0.92, // Na+/K+ ATPase affectedSystems:['cardiovascular'] }, { id:'omeprazole', name:'Omeprazole', mw:345, logp:2.2, proteinBinding:0.95, cypSubstrates:['CYP2C19'], cypInhibitors:['CYP2C19'], pgpSubstrate:false, pgpInhibitor:false, halfLife:0.7, tmax:0.5, bioavailability:0.40, therapeuticIndex:'wide', organEffects:['Acid suppression','Vitamin B12 deficiency'], clearance:1.2, dose_mg:20, color:'#d9a85f', receptorAffinity:0.88, // proton pump affectedSystems:['gi'] }, { id:'clopidogrel', name:'Clopidogrel', mw:322, logp:3.8, proteinBinding:0.98, cypSubstrates:['CYP2C19'], cypInhibitors:[], pgpSubstrate:false, pgpInhibitor:false, halfLife:6, tmax:1, bioavailability:0.50, therapeuticIndex:'moderate', organEffects:['Antiplatelet','Bleeding risk'], clearance:0.45, dose_mg:75, color:'#b07c44', receptorAffinity:0.82, // P2Y12 irreversible affectedSystems:['coagulation'] }, { id:'diazepam', name:'Diazepam', mw:285, logp:2.8, proteinBinding:0.98, cypSubstrates:['CYP3A4','CYP2C19'], cypInhibitors:[], pgpSubstrate:false, pgpInhibitor:false, halfLife:36, tmax:1.5, bioavailability:1.0, therapeuticIndex:'wide', organEffects:['Sedation','Anxiolysis','Respiratory depression'], clearance:0.19, dose_mg:10, color:'#9c7a55', receptorAffinity:0.90, // GABA-A affectedSystems:['cns'] }, { id:'ibuprofen', name:'Ibuprofen', mw:206, logp:3.5, proteinBinding:0.99, cypSubstrates:['CYP2C9'], cypInhibitors:[], pgpSubstrate:false, pgpInhibitor:false, halfLife:2, tmax:1, bioavailability:0.80, therapeuticIndex:'wide', organEffects:['Anti-inflammatory','GI bleeding','Renal impairment'], clearance:0.90, dose_mg:400, color:'#d6a23a', receptorAffinity:0.70, // COX inhibition affectedSystems:['coagulation','gi','renal'] } ]; // ─── CYP INHIBITION POTENCIES (Ki-like scaling) ─── // How strongly each inhibitor affects each CYP enzyme // Values represent fractional inhibition at clinical dose (0-1) const CYP_INHIBITION_POTENCY = { 'fluconazole': {'CYP2C9':0.65, 'CYP3A4':0.40}, 'amiodarone': {'CYP3A4':0.55, 'CYP2C9':0.50}, 'clarithromycin':{'CYP3A4':0.75}, 'omeprazole': {'CYP2C19':0.60} }; // ─── STATE ─── let selectedDrugs = []; // ─── PHARMACOLOGY ENGINE ─── function computeInteractions(drugs) { const results = []; const pairResults = []; for (let i = 0; i < drugs.length; i++) { for (let j = i+1; j < drugs.length; j++) { const a = drugs[i], b = drugs[j]; const pair = computePairwise(a, b); pairResults.push(pair); } } // Aggregate per-drug effects for (const drug of drugs) { let aucMultiplier = 1.0; let cmaxMultiplier = 1.0; let freeFractionIncrease = 0; const mechanisms = []; const cypDetails = []; for (const pr of pairResults) { if (pr.drugA.id === drug.id) { aucMultiplier *= pr.aucEffectOnA; cmaxMultiplier *= pr.cmaxEffectOnA; freeFractionIncrease += pr.freeFractionIncreaseA; mechanisms.push(...pr.mechanismsA); cypDetails.push(...pr.cypDetailsA); } if (pr.drugB.id === drug.id) { aucMultiplier *= pr.aucEffectOnB; cmaxMultiplier *= pr.cmaxEffectOnB; freeFractionIncrease += pr.freeFractionIncreaseB; mechanisms.push(...pr.mechanismsB); cypDetails.push(...pr.cypDetailsB); } } results.push({ drug, aucMultiplier: Math.min(aucMultiplier, 8), cmaxMultiplier: Math.min(cmaxMultiplier, 5), freeFractionIncrease: Math.min(freeFractionIncrease, 0.5), mechanisms: [...new Set(mechanisms)], cypDetails }); } // Compute overall severity const severity = computeSeverity(results, pairResults); // Compute PD additive effects const pdEffects = computePDEffects(drugs, results); return { perDrug: results, pairs: pairResults, severity, pdEffects }; } function computePairwise(a, b) { // CYP-mediated interactions let aucEffectOnA = 1.0, aucEffectOnB = 1.0; let cmaxEffectOnA = 1.0, cmaxEffectOnB = 1.0; const mechanismsA = [], mechanismsB = []; const cypDetailsA = [], cypDetailsB = []; // Does B inhibit any CYP that A is a substrate of? for (const sub of a.cypSubstrates) { const inhib = getCYPInhibition(b, sub); if (inhib > 0) { // AUC increase = 1/(1 - inhibition_fraction) const factor = 1 / (1 - inhib); aucEffectOnA *= factor; cmaxEffectOnA *= (1 + (factor - 1) * 0.6); // Cmax doesn't rise as much as AUC mechanismsA.push(`CYP inhibition`); cypDetailsA.push({enzyme: sub, inhibitor: b.name, inhibited: a.name, fraction: inhib, aucFactor: factor}); } } // Does A inhibit any CYP that B is a substrate of? for (const sub of b.cypSubstrates) { const inhib = getCYPInhibition(a, sub); if (inhib > 0) { const factor = 1 / (1 - inhib); aucEffectOnB *= factor; cmaxEffectOnB *= (1 + (factor - 1) * 0.6); mechanismsB.push(`CYP inhibition`); cypDetailsB.push({enzyme: sub, inhibitor: a.name, inhibited: b.name, fraction: inhib, aucFactor: factor}); } } // P-gp interactions if (a.pgpInhibitor && b.pgpSubstrate) { const pgpFactor = 1.6; aucEffectOnB *= pgpFactor; cmaxEffectOnB *= 1.3; mechanismsB.push('P-gp inhibition'); cypDetailsB.push({enzyme:'P-gp', inhibitor:a.name, inhibited:b.name, fraction:0.38, aucFactor:pgpFactor}); } if (b.pgpInhibitor && a.pgpSubstrate) { const pgpFactor = 1.6; aucEffectOnA *= pgpFactor; cmaxEffectOnA *= 1.3; mechanismsA.push('P-gp inhibition'); cypDetailsA.push({enzyme:'P-gp', inhibitor:b.name, inhibited:a.name, fraction:0.38, aucFactor:pgpFactor}); } // Protein binding displacement let freeFractionIncreaseA = 0, freeFractionIncreaseB = 0; if (a.proteinBinding > 0.7 && b.proteinBinding > 0.7) { // Both highly protein bound → displacement competition const fa = 1 - a.proteinBinding; // free fraction const fb = 1 - b.proteinBinding; // Displacement modeled by competitive binding to albumin // The drug with higher LogP tends to displace more const logPRatio = b.logp / (a.logp + b.logp); const displacementOnA = logPRatio * fb * 2.5; // scaled const logPRatio2 = a.logp / (a.logp + b.logp); const displacementOnB = logPRatio2 * fa * 2.5; freeFractionIncreaseA = Math.min(displacementOnA, 0.3); freeFractionIncreaseB = Math.min(displacementOnB, 0.3); if (freeFractionIncreaseA > 0.02) mechanismsA.push('Protein displacement'); if (freeFractionIncreaseB > 0.02) mechanismsB.push('Protein displacement'); } return { drugA: a, drugB: b, aucEffectOnA, aucEffectOnB, cmaxEffectOnA, cmaxEffectOnB, freeFractionIncreaseA, freeFractionIncreaseB, mechanismsA, mechanismsB, cypDetailsA, cypDetailsB }; } function getCYPInhibition(drug, enzyme) { const pot = CYP_INHIBITION_POTENCY[drug.id]; if (!pot) return 0; return pot[enzyme] || 0; } function computeSeverity(perDrug, pairs) { let maxSeverity = 'green'; // default let details = []; let guidance = []; for (const pd of perDrug) { const auc = pd.aucMultiplier; const narrow = pd.drug.therapeuticIndex === 'narrow'; const moderate = pd.drug.therapeuticIndex === 'moderate'; if (auc > 3.0 && narrow) { if (maxSeverity !== 'black') maxSeverity = 'red'; details.push(`${pd.drug.name}: AUC ${auc.toFixed(1)}x increase (narrow TI)`); guidance.push(`Illustrative threshold reached for ${pd.drug.name}; this is not a clinical risk classification or dosing recommendation.`); } else if (auc > 2.0 && narrow) { if (['green','yellow'].includes(maxSeverity)) maxSeverity = 'red'; details.push(`${pd.drug.name}: AUC ${auc.toFixed(1)}x increase (narrow TI)`); guidance.push(`Illustrative threshold reached for ${pd.drug.name}; this is not a clinical risk classification or dosing recommendation.`); } else if (auc > 2.0 && moderate) { if (['green','yellow'].includes(maxSeverity)) maxSeverity = 'orange'; details.push(`${pd.drug.name}: AUC ${auc.toFixed(1)}x increase`); guidance.push(`Illustrative threshold reached for ${pd.drug.name}; this is not a clinical risk classification or dosing recommendation.`); } else if (auc > 1.5 && narrow) { if (['green'].includes(maxSeverity)) maxSeverity = 'orange'; details.push(`${pd.drug.name}: AUC ${auc.toFixed(1)}x increase (narrow TI)`); guidance.push(`Illustrative threshold reached for ${pd.drug.name}; this is not a clinical risk classification or dosing recommendation.`); } else if (auc > 1.3) { if (maxSeverity === 'green') maxSeverity = 'yellow'; details.push(`${pd.drug.name}: AUC ${auc.toFixed(1)}x increase`); guidance.push(`Illustrative threshold reached for ${pd.drug.name}; this is not a clinical risk classification or dosing recommendation.`); } else { details.push(`${pd.drug.name}: No threshold crossing in this simplified PK model`); } // Protein displacement severity if (pd.freeFractionIncrease > 0.1 && narrow) { if (['green','yellow'].includes(maxSeverity)) maxSeverity = 'orange'; details.push(`${pd.drug.name}: Free fraction increased ${(pd.freeFractionIncrease*100).toFixed(0)}% by displacement`); guidance.push(`Illustrative threshold reached for ${pd.drug.name}; this is not a clinical risk classification or dosing recommendation.`); } else if (pd.freeFractionIncrease > 0.05) { if (maxSeverity === 'green') maxSeverity = 'yellow'; details.push(`${pd.drug.name}: Free fraction increased ${(pd.freeFractionIncrease*100).toFixed(0)}% by displacement`); } } // Upper illustrative exposure bin, not a clinical contraindication classification for (const pd of perDrug) { if (pd.aucMultiplier > 4.0 && pd.drug.therapeuticIndex === 'narrow') { maxSeverity = 'black'; guidance.unshift(`Large modeled exposure shift for ${pd.drug.name}; this heuristic cannot quantify patient risk.`); } } return { level: maxSeverity, details, guidance: guidance.join(' ') || 'No threshold crossing in this simplified model does not establish safety.', label: { green:'Below illustrative thresholds', yellow:'Small model signal', orange:'Intermediate model signal', red:'Large model signal', black:'Very large model signal' }[maxSeverity] }; } function computePDEffects(drugs, perDrug) { const effects = []; const systemMap = {}; for (const drug of drugs) { for (const sys of drug.affectedSystems) { if (!systemMap[sys]) systemMap[sys] = []; systemMap[sys].push(drug); } } for (const [system, drugsInSys] of Object.entries(systemMap)) { if (drugsInSys.length < 2) continue; // Bliss independence: E_combined = E_A + E_B - E_A * E_B const effects_drugs = drugsInSys.map(d => { const pd = perDrug.find(p => p.drug.id === d.id); const boosted = pd ? pd.aucMultiplier : 1; return Math.min(d.receptorAffinity * Math.min(boosted, 3) * 0.5, 1); }); let combined = effects_drugs[0]; for (let i = 1; i < effects_drugs.length; i++) { combined = combined + effects_drugs[i] - combined * effects_drugs[i]; } const sysName = { coagulation:'Coagulation System', cardiovascular:'Cardiovascular System', cns:'Central Nervous System', gi:'Gastrointestinal System', musculoskeletal:'Musculoskeletal System', hepatic:'Hepatic System', renal:'Renal System', thyroid:'Thyroid', fungal:'Antifungal model component' }[system] || system; let risk = 'low'; if (combined > 0.8) risk = 'high'; else if (combined > 0.5) risk = 'moderate'; effects.push({ system: sysName, drugs: drugsInSys.map(d => d.name), combinedEffect: combined, risk, model: 'Bliss Independence' }); } return effects; } // ─── PK CURVE COMPUTATION ─── // One-compartment oral model: C(t) = (F*D/V) * (ka/(ka-ke)) * (e^(-ke*t) - e^(-ka*t)) function computeConcCurve(drug, aucMultiplier, hours, nPoints) { const ka = Math.log(2) / (drug.tmax / 2.5); // absorption half-life approximation const ke = Math.log(2) / drug.halfLife; const F = drug.bioavailability; const D = drug.dose_mg * 1000; // mcg const V = drug.mw * 0.8; // rough volume in L const points = []; const dt = hours / nPoints; for (let i = 0; i <= nPoints; i++) { const t = i * dt; let C; if (t === 0) { C = 0; } else { const denom = ka - ke; C = (F * D / V) * (ka / Math.abs(denom)) * (Math.exp(-ke * t) - Math.exp(-ka * t)); } // Apply interaction multiplier (time-varying: peaks during absorption) // The interaction primarily affects clearance, so the multiplier grows over time const timeFactor = 1 + (aucMultiplier - 1) * (1 - Math.exp(-0.1 * t)); points.push({ t, C_normal: C, C_interacted: C * timeFactor }); } return points; } function computeOccupancy(drug, aucMultiplier, hours, nPoints) { const ka = Math.log(2) / (drug.tmax / 2.5); const ke = Math.log(2) / drug.halfLife; const F = drug.bioavailability; const D = drug.dose_mg * 1000; const V = drug.mw * 0.8; const Kd = drug.mw * 0.15; // dissociation constant scaled const points = []; const dt = hours / nPoints; for (let i = 0; i <= nPoints; i++) { const t = i * dt; let C; if (t === 0) { C = 0; } else { const denom = ka - ke; C = (F * D / V) * (ka / Math.abs(denom)) * (Math.exp(-ke * t) - Math.exp(-ka * t)); } const timeFactor = 1 + (aucMultiplier - 1) * (1 - Math.exp(-0.1 * t)); const Ci = C * timeFactor; // Emax model for occupancy const occ_normal = (C / (C + Kd)) * drug.receptorAffinity * 100; const occ_interacted = (Ci / (Ci + Kd)) * drug.receptorAffinity * 100; points.push({ t, occ_normal: Math.min(occ_normal, 100), occ_interacted: Math.min(occ_interacted, 100) }); } return points; } // ─── RENDERING ─── function renderDrugList() { const el = document.getElementById('drugList'); const focusedId = document.activeElement && document.activeElement.getAttribute('data-drug-id'); el.innerHTML = ''; DRUGS.forEach((drug, idx) => { const sel = selectedDrugs.find(d => d.id === drug.id); const full = selectedDrugs.length >= 3 && !sel; const card = document.createElement('button'); card.type = 'button'; card.setAttribute('data-drug-id', drug.id); card.disabled = !!full; card.setAttribute('aria-disabled', String(!!full)); card.setAttribute('aria-pressed', String(selectedDrugs.some(d => d.id === drug.id))); card.className = 'drug-card' + (sel ? ' selected' : '') + (full ? ' disabled' : ''); const tags = [ ...drug.cypSubstrates.map(s => `${s} sub`), ...drug.cypInhibitors.map(s => `${s} inh`), ...(drug.pgpSubstrate ? [`P-gp sub`] : []), ...(drug.pgpInhibitor ? [`P-gp inh`] : []) ].join(''); card.innerHTML = `
${sel ? `${selectedDrugs.indexOf(sel)+1}` : ''}
${drug.name}
MW ${drug.mw} | LogP ${drug.logp} | ${(drug.proteinBinding*100).toFixed(0)}% bound
${tags}
`; card.addEventListener('click', () => toggleDrug(drug)); el.appendChild(card); }); if (focusedId) { const replacement = Array.from(el.children).find(card => card.getAttribute('data-drug-id') === focusedId); if (replacement && !replacement.disabled) replacement.focus(); } } function toggleDrug(drug) { const idx = selectedDrugs.findIndex(d => d.id === drug.id); if (idx >= 0) { selectedDrugs.splice(idx, 1); } else if (selectedDrugs.length < 3) { selectedDrugs.push(drug); } renderDrugList(); if (selectedDrugs.length >= 2) { runSimulation(); } else { clearSimulation(); } } function clearSimulation() { document.getElementById('chartPlaceholder').style.display = 'flex'; document.getElementById('concCanvas').style.display = 'none'; document.getElementById('chartBadge').style.display = 'none'; document.getElementById('receptorSection').style.display = 'none'; document.getElementById('mechanismContent').innerHTML = `
Interaction mechanisms will appear here once drugs are selected.
`; document.getElementById('severityContent').innerHTML = `
Select drugs to assess interaction severity.
`; renderProps(); } function runSimulation() { const interaction = computeInteractions(selectedDrugs); renderConcChart(interaction); renderOccupancyChart(interaction); renderMechanisms(interaction); renderSeverity(interaction); renderProps(); renderOrganEffects(interaction); } function renderConcChart(interaction) { const canvas = document.getElementById('concCanvas'); const placeholder = document.getElementById('chartPlaceholder'); const badge = document.getElementById('chartBadge'); placeholder.style.display = 'none'; canvas.style.display = 'block'; badge.style.display = 'inline'; badge.textContent = `${selectedDrugs.length} drugs | 24h`; const dpr = window.devicePixelRatio || 1; const rect = canvas.parentElement.getBoundingClientRect(); const w = rect.width - 32; const h = 360; canvas.width = w * dpr; canvas.height = h * dpr; canvas.style.width = w + 'px'; canvas.style.height = h + 'px'; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); ctx.clearRect(0, 0, w, h); const margin = {top:30, right:30, bottom:50, left:60}; const pw = w - margin.left - margin.right; const ph = h - margin.top - margin.bottom; // Background (transparent so warm dark parent shows through) // ctx.fillStyle previously '#fafbfc', removed for dark-theme consistency // Grid (warm low-contrast) ctx.strokeStyle = 'rgba(244, 239, 230, 0.08)'; ctx.lineWidth = 0.5; for (let i = 0; i <= 6; i++) { const x = margin.left + (i / 6) * pw; ctx.beginPath(); ctx.moveTo(x, margin.top); ctx.lineTo(x, margin.top + ph); ctx.stroke(); } for (let i = 0; i <= 5; i++) { const y = margin.top + (i / 5) * ph; ctx.beginPath(); ctx.moveTo(margin.left, y); ctx.lineTo(margin.left + pw, y); ctx.stroke(); } // Title ctx.fillStyle = '#d5744f'; ctx.font = '600 12px Inter, system-ui, sans-serif'; ctx.fillText('Plasma Concentration vs Time', margin.left, margin.top - 12); // Axes labels ctx.fillStyle = 'rgba(244, 239, 230, 0.55)'; ctx.font = '500 10px Inter, system-ui, sans-serif'; ctx.textAlign = 'center'; for (let i = 0; i <= 6; i++) { const hr = (i / 6) * 24; ctx.fillText(hr + 'h', margin.left + (i / 6) * pw, margin.top + ph + 18); } ctx.fillText('Time (hours)', margin.left + pw / 2, margin.top + ph + 38); ctx.textAlign = 'right'; ctx.fillText('Conc', margin.left - 8, margin.top - 2); // Compute curves let globalMax = 0; const curves = []; for (const pd of interaction.perDrug) { const pts = computeConcCurve(pd.drug, pd.aucMultiplier, 24, 200); for (const p of pts) { globalMax = Math.max(globalMax, p.C_normal, p.C_interacted); } curves.push({ pts, drug: pd.drug, pd }); } if (globalMax === 0) globalMax = 1; // Draw curves for (const curve of curves) { const {pts, drug, pd} = curve; // Normal curve (dashed, lighter) ctx.strokeStyle = drug.color + '55'; ctx.lineWidth = 1.5; ctx.setLineDash([4, 3]); ctx.beginPath(); for (let i = 0; i < pts.length; i++) { const x = margin.left + (pts[i].t / 24) * pw; const y = margin.top + ph - (pts[i].C_normal / globalMax) * ph; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); ctx.setLineDash([]); // Interacted curve (solid) ctx.strokeStyle = drug.color; ctx.lineWidth = 2.5; ctx.beginPath(); for (let i = 0; i < pts.length; i++) { const x = margin.left + (pts[i].t / 24) * pw; const y = margin.top + ph - (pts[i].C_interacted / globalMax) * ph; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } // Legend const lx = margin.left + 10; let ly = margin.top + 16; for (const curve of curves) { ctx.fillStyle = curve.drug.color; ctx.fillRect(lx, ly - 4, 16, 3); ctx.fillStyle = '#f4efe6'; ctx.font = '600 10px Inter, system-ui, sans-serif'; ctx.textAlign = 'left'; const aucTxt = curve.pd.aucMultiplier > 1.05 ? ` (${curve.pd.aucMultiplier.toFixed(1)}x AUC)` : ''; ctx.fillText(`${curve.drug.name}${aucTxt}`, lx + 22, ly); ly += 16; } // Dashed legend ctx.strokeStyle = 'rgba(244, 239, 230, 0.45)'; ctx.lineWidth = 1.5; ctx.setLineDash([4, 3]); ctx.beginPath(); ctx.moveTo(lx, ly); ctx.lineTo(lx + 16, ly); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = 'rgba(244, 239, 230, 0.55)'; ctx.font = '400 9px Inter, system-ui, sans-serif'; ctx.fillText('Without interaction', lx + 22, ly + 1); // Y-axis ticks ctx.textAlign = 'right'; ctx.fillStyle = 'rgba(244, 239, 230, 0.55)'; ctx.font = '400 9px Inter, system-ui, sans-serif'; for (let i = 0; i <= 5; i++) { const val = (globalMax * (1 - i/5)).toFixed(1); ctx.fillText(val, margin.left - 6, margin.top + (i/5)*ph + 3); } } function renderOccupancyChart(interaction) { const section = document.getElementById('receptorSection'); section.style.display = 'block'; const canvas = document.getElementById('occCanvas'); const dpr = window.devicePixelRatio || 1; const rect = canvas.parentElement.getBoundingClientRect(); const w = rect.width - 32; const h = 260; canvas.width = w * dpr; canvas.height = h * dpr; canvas.style.width = w + 'px'; canvas.style.height = h + 'px'; const ctx = canvas.getContext('2d'); ctx.scale(dpr, dpr); ctx.clearRect(0, 0, w, h); const margin = {top:30, right:30, bottom:40, left:60}; const pw = w - margin.left - margin.right; const ph = h - margin.top - margin.bottom; ctx.strokeStyle = 'rgba(244, 239, 230, 0.08)'; ctx.lineWidth = 0.5; for (let i = 0; i <= 6; i++) { const x = margin.left + (i / 6) * pw; ctx.beginPath(); ctx.moveTo(x, margin.top); ctx.lineTo(x, margin.top + ph); ctx.stroke(); } for (let i = 0; i <= 5; i++) { const y = margin.top + (i / 5) * ph; ctx.beginPath(); ctx.moveTo(margin.left, y); ctx.lineTo(margin.left + pw, y); ctx.stroke(); } ctx.fillStyle = '#d5744f'; ctx.font = '600 12px Inter, system-ui, sans-serif'; ctx.fillText('Receptor Occupancy (%)', margin.left, margin.top - 12); ctx.textAlign = 'center'; ctx.fillStyle = 'rgba(244, 239, 230, 0.55)'; ctx.font = '500 10px Inter, system-ui, sans-serif'; for (let i = 0; i <= 6; i++) { ctx.fillText((i/6*24)+'h', margin.left + (i/6)*pw, margin.top + ph + 18); } ctx.textAlign = 'right'; for (let i = 0; i <= 5; i++) { ctx.fillText((100 - i*20)+'%', margin.left - 6, margin.top + (i/5)*ph + 3); } for (const pd of interaction.perDrug) { const pts = computeOccupancy(pd.drug, pd.aucMultiplier, 24, 200); // Normal ctx.strokeStyle = pd.drug.color + '44'; ctx.lineWidth = 1.5; ctx.setLineDash([4, 3]); ctx.beginPath(); for (let i = 0; i < pts.length; i++) { const x = margin.left + (pts[i].t / 24) * pw; const y = margin.top + ph - (pts[i].occ_normal / 100) * ph; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); ctx.setLineDash([]); // Interacted ctx.strokeStyle = pd.drug.color; ctx.lineWidth = 2; ctx.beginPath(); for (let i = 0; i < pts.length; i++) { const x = margin.left + (pts[i].t / 24) * pw; const y = margin.top + ph - (pts[i].occ_interacted / 100) * ph; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } // Legend let ly = margin.top + 16; const lx = margin.left + pw - 150; for (const pd of interaction.perDrug) { ctx.fillStyle = pd.drug.color; ctx.fillRect(lx, ly - 4, 12, 3); ctx.fillStyle = '#f4efe6'; ctx.font = '500 9px Inter, system-ui, sans-serif'; ctx.textAlign = 'left'; ctx.fillText(pd.drug.name, lx + 16, ly); ly += 14; } } function renderMechanisms(interaction) { const el = document.getElementById('mechanismContent'); if (interaction.pairs.length === 0) { el.innerHTML = `
No modeled interaction signal between selected drugs. This does not establish safety.
`; return; } let html = '
'; // CYP mechanisms for (const pair of interaction.pairs) { const allCYP = [...pair.cypDetailsA, ...pair.cypDetailsB]; for (const cyp of allCYP) { html += `
⚙ CYP Enzyme Inhibition: ${cyp.enzyme}

${cyp.inhibitor} inhibits ${cyp.enzyme}, which metabolizes ${cyp.inhibited}. This reduces ${cyp.inhibited} clearance by ${(cyp.fraction*100).toFixed(0)}%, increasing AUC by ${(cyp.aucFactor).toFixed(1)}x.

AUC_new = AUC_base / (1 - ${cyp.fraction.toFixed(2)}) = ${cyp.aucFactor.toFixed(2)}x
`; } // P-gp mechanism const hasPgpa = pair.mechanismsA.includes('P-gp inhibition'); const hasPgpB = pair.mechanismsB.includes('P-gp inhibition'); if (hasPgpa || hasPgpB) { const inhibitor = hasPgpa ? pair.drugA : pair.drugB; const substrate = hasPgpa ? pair.drugB : pair.drugA; html += `
▶ P-glycoprotein Inhibition

${inhibitor.name} inhibits P-gp transport, reducing efflux of ${substrate.name}. This increases absorption and decreases renal/biliary clearance, raising systemic exposure ~1.6x.

CL_pgpgp_inhibited = CL_pgp * (1 - f_inhibit)
`; } // Protein displacement if (pair.freeFractionIncreaseA > 0.02 || pair.freeFractionIncreaseB > 0.02) { const displaced = pair.freeFractionIncreaseA > pair.freeFractionIncreaseB ? pair.drugA : pair.drugB; const displacer = pair.freeFractionIncreaseA > pair.freeFractionIncreaseB ? pair.drugB : pair.drugA; const increase = Math.max(pair.freeFractionIncreaseA, pair.freeFractionIncreaseB); html += `
◆ Plasma Protein Binding Displacement

${displacer.name} (LogP ${displacer.logp}, ${(displacer.proteinBinding*100).toFixed(0)}% bound) displaces ${displaced.name} (LogP ${displaced.logp}, ${(displaced.proteinBinding*100).toFixed(0)}% bound) from albumin binding sites. Free fraction of ${displaced.name} increases by ~${(increase*100).toFixed(0)}%, amplifying its pharmacological effect.

f_free_new = f_free_base + displacement = ${(displaced.proteinBinding < 1 ? ((1-displaced.proteinBinding) + increase)*100 : increase*100).toFixed(1)}%
`; } } // PD additive effects for (const pd of interaction.pdEffects) { if (pd.risk === 'low') continue; html += `
⚡ Additive Pharmacodynamic Effect: ${pd.system}

${pd.drugs.join(' + ')} both affect the ${pd.system}. Combined effect computed via ${pd.model}: E = ${(pd.combinedEffect*100).toFixed(0)}% (illustrative signal: ${pd.risk}).

E_comb = E_A + E_B - E_A * E_B = ${(pd.combinedEffect*100).toFixed(0)}%
`; } // If no mechanisms found if (!html.includes('mech-card')) { html += `

No mechanism crossed the display threshold in this simplified model. Real interactions may be absent from the model.

`; } html += '
'; el.innerHTML = html; } function renderSeverity(interaction) { const el = document.getElementById('severityContent'); const sev = interaction.severity; if (sev.level === 'green' && selectedDrugs.length >= 2) { // Check if truly no interactions const hasAny = interaction.perDrug.some(pd => pd.aucMultiplier > 1.1 || pd.freeFractionIncrease > 0.02); if (!hasAny && interaction.pdEffects.length === 0) { el.innerHTML = `
${sev.label}
No signal was produced by these limited equations. This does not rule out an interaction or establish safety.
`; return; } } let detailsHtml = sev.details.map(d => `
${d}
`).join(''); el.innerHTML = `
${sev.label}
${sev.guidance}
${detailsHtml}
`; } function renderProps() { const el = document.getElementById('propPanel'); if (selectedDrugs.length === 0) { el.innerHTML = `
Selected drug properties will appear here.
`; return; } let html = ''; for (const drug of selectedDrugs) { const cypTags = [ ...drug.cypSubstrates.map(s => `${s} substrate`), ...drug.cypInhibitors.map(s => `${s} inhibitor`), ...(drug.pgpSubstrate ? [`P-gp substrate`] : []), ...(drug.pgpInhibitor ? [`P-gp inhibitor`] : []) ].join(''); const tiColor = drug.therapeuticIndex === 'narrow' ? '#e0795f' : drug.therapeuticIndex === 'moderate' ? '#d5744f' : '#8fb079'; html += `
${drug.name}
MW${drug.mw} Da
LogP${drug.logp}
Protein Binding${(drug.proteinBinding*100).toFixed(0)}%
Half-life${drug.halfLife} h
T_max${drug.tmax} h
Bioavailability${(drug.bioavailability*100).toFixed(0)}%
Therapeutic Index${drug.therapeuticIndex}
CYP / Transporter Profile
${cypTags}
Organ Effects
${drug.organEffects.join(', ')}
`; } el.innerHTML = html; } function renderOrganEffects(interaction) { const el = document.getElementById('organEffects'); if (interaction.pdEffects.length === 0) { el.innerHTML = '
No additive organ effects detected.
'; return; } // Monochrome line glyphs (inherit white/coral text color) instead of color emoji const icons = { 'Coagulation System':'✚', 'Cardiovascular System':'♥︎', 'Central Nervous System':'❖', 'Gastrointestinal System':'▣', 'Musculoskeletal System':'◆', 'Hepatic System':'◉', 'Renal System':'⬡', 'Thyroid':'✦' }; el.innerHTML = interaction.pdEffects.map(e => `
${icons[e.system]||'✚'} ${e.system} ${e.drugs.join(' + ')} | Effect: ${(e.combinedEffect*100).toFixed(0)}% | ${e.risk} illustrative signal
`).join(''); } // ─── INIT ─── renderDrugList(); clearSimulation(); // Pre-load Warfarin + Fluconazole, highest-severity DDI in the set (CYP2C9 inhibition) (function() { const w = DRUGS.find(d => d.id === 'warfarin'); const f = DRUGS.find(d => d.id === 'fluconazole'); if (w && f) { selectedDrugs.push(w, f); renderDrugList(); runSimulation(); } })();