hyphy-scope
Version:
Reusable Svelte components for HyPhy analysis visualization
283 lines (282 loc) • 9.24 kB
JavaScript
/**
* ABSREL plotting functions using Observable Plot
*/
import * as Plot from "@observablehq/plot";
/**
* Create a p-value significance plot
*/
export function createPValuePlot(branches, threshold = 0.05) {
if (!branches.length)
return null;
// Transform data for plotting
const plotData = branches.map((branch, index) => ({
branch: branch.name,
index: index,
uncorrectedPValue: branch['Uncorrected P-value'],
correctedPValue: branch['Corrected P-value'],
logUncorrected: -Math.log10(Math.max(branch['Uncorrected P-value'], 1e-10)),
logCorrected: -Math.log10(Math.max(branch['Corrected P-value'], 1e-10)),
significant: branch['Corrected P-value'] <= threshold
}));
const thresholdLine = -Math.log10(threshold);
return Plot.plot({
title: "Branch Selection Significance",
subtitle: `p-value threshold: ${threshold}`,
width: 800,
height: 400,
marginLeft: 100,
marginBottom: 120,
x: {
type: "band",
domain: plotData.map(d => d.branch),
label: "Branch",
tickRotate: -45
},
y: {
label: "-log₁₀(p-value)",
grid: true
},
color: {
legend: true,
range: ["#1f77b4", "#ff7f0e"]
},
marks: [
// Threshold line
Plot.ruleY([thresholdLine], {
stroke: "#d62728",
strokeDasharray: "5,5",
strokeWidth: 2
}),
// Uncorrected p-values
Plot.dot(plotData, {
x: "branch",
y: "logUncorrected",
fill: "#1f77b4",
r: 6,
title: d => `${d.branch}\nUncorrected: ${d.uncorrectedPValue.toExponential(2)}`
}),
// Corrected p-values
Plot.dot(plotData, {
x: "branch",
y: "logCorrected",
fill: "#ff7f0e",
r: 6,
title: d => `${d.branch}\nCorrected: ${d.correctedPValue.toExponential(2)}`
}),
// Highlight significant branches
Plot.dot(plotData.filter(d => d.significant), {
x: "branch",
y: "logCorrected",
stroke: "#d62728",
strokeWidth: 3,
fill: "none",
r: 8
})
]
});
}
/**
* Create a Bayes Factor plot
*/
export function createBayesFactorPlot(branches) {
if (!branches.length)
return null;
const plotData = branches.map((branch, index) => ({
branch: branch.name,
index: index,
bayesFactor: branch['Bayes Factor'],
logBayesFactor: Math.log10(Math.max(branch['Bayes Factor'], 1e-10)),
evidenceLevel: getEvidenceLevel(branch['Bayes Factor'])
}));
return Plot.plot({
title: "Bayes Factor Evidence for Selection",
subtitle: "Higher values indicate stronger evidence for selection",
width: 800,
height: 400,
marginLeft: 100,
marginBottom: 120,
x: {
type: "band",
domain: plotData.map(d => d.branch),
label: "Branch",
tickRotate: -45
},
y: {
label: "log₁₀(Bayes Factor)",
grid: true
},
color: {
type: "ordinal",
domain: ["Very Strong", "Strong", "Substantial", "Weak", "No Evidence"],
range: ["#8B0000", "#DC143C", "#FF6347", "#FFA500", "#808080"]
},
marks: [
// Reference lines for evidence levels
Plot.ruleY([0], { stroke: "#666", strokeDasharray: "3,3" }), // BF = 1
Plot.ruleY([Math.log10(3)], { stroke: "#999", strokeDasharray: "3,3" }), // BF = 3 (substantial)
Plot.ruleY([Math.log10(10)], { stroke: "#999", strokeDasharray: "3,3" }), // BF = 10 (strong)
Plot.ruleY([Math.log10(100)], { stroke: "#999", strokeDasharray: "3,3" }), // BF = 100 (very strong)
// Bars
Plot.barY(plotData, {
x: "branch",
y: "logBayesFactor",
fill: "evidenceLevel",
title: d => `${d.branch}\nBayes Factor: ${d.bayesFactor.toFixed(2)}\nEvidence: ${d.evidenceLevel}`
})
]
});
}
/**
* Create omega distribution plot for a specific branch
*/
export function createOmegaDistributionPlot(branch) {
if (!branch['ω distribution'] || !branch['ω distribution'].length)
return null;
const plotData = branch['ω distribution'].map((dist, index) => ({
rateClass: dist['rate class'] || index + 1,
omega: dist.omega,
weight: dist.weight,
category: dist.omega > 1 ? 'Positive Selection (ω > 1)' :
dist.omega === 1 ? 'Neutral (ω = 1)' : 'Purifying Selection (ω < 1)'
}));
return Plot.plot({
title: `ω Distribution for ${branch.name}`,
subtitle: "Rate classes and their weights",
width: 600,
height: 300,
marginLeft: 80,
x: {
label: "Rate Class",
grid: true
},
y: {
label: "ω (dN/dS)",
grid: true
},
color: {
type: "ordinal",
domain: ["Purifying Selection (ω < 1)", "Neutral (ω = 1)", "Positive Selection (ω > 1)"],
range: ["#1f77b4", "#2ca02c", "#d62728"]
},
marks: [
// Reference line at ω = 1
Plot.ruleY([1], { stroke: "#666", strokeDasharray: "3,3" }),
// Points sized by weight
Plot.dot(plotData, {
x: "rateClass",
y: "omega",
fill: "category",
r: d => Math.sqrt(d.weight) * 20,
title: d => `Rate Class ${d.rateClass}\nω = ${d.omega.toFixed(3)}\nWeight = ${d.weight.toFixed(3)}`
})
]
});
}
/**
* Create site-level log likelihood plot
*/
export function createSiteLogLikelihoodPlot(siteData, selectedBranches = []) {
if (!siteData.length)
return null;
// Get all branch names (excluding 'site' and 'partition')
const branchNames = Object.keys(siteData[0]).filter(key => key !== 'site' && key !== 'partition');
// Use selected branches or all branches if none selected
const activeBranches = selectedBranches.length > 0 ? selectedBranches : branchNames.slice(0, 3);
// Transform data for plotting
const plotData = siteData.flatMap(site => activeBranches.map(branch => ({
site: site.site,
branch: branch,
logLikelihood: site[branch] || 0
})));
return Plot.plot({
title: "Site-Level Log Likelihood",
subtitle: `Showing ${activeBranches.join(', ')}`,
width: 1000,
height: 400,
marginLeft: 80,
x: {
label: "Site",
grid: true
},
y: {
label: "Log Likelihood",
grid: true
},
color: {
legend: true
},
marks: [
Plot.line(plotData, {
x: "site",
y: "logLikelihood",
stroke: "branch",
strokeWidth: 2
}),
Plot.dot(plotData.filter((d, i) => i % 10 === 0), {
x: "site",
y: "logLikelihood",
fill: "branch",
r: 3
})
]
});
}
/**
* Create model comparison plot
*/
export function createModelComparisonPlot(results) {
const baseline = results.fits['Baseline model'];
const fullModel = results.fits['Full adaptive model'];
const plotData = [
{
model: "Baseline",
logLikelihood: baseline['log-likelihood'],
AIC: baseline.AIC,
parameters: baseline.parameters
},
{
model: "Full Adaptive",
logLikelihood: fullModel['log-likelihood'],
AIC: fullModel.AIC,
parameters: fullModel.parameters
}
];
return Plot.plot({
title: "Model Comparison",
subtitle: "Log Likelihood and AIC values",
width: 500,
height: 300,
marginLeft: 100,
x: {
type: "band",
domain: ["Baseline", "Full Adaptive"],
label: "Model"
},
y: {
label: "Log Likelihood",
grid: true
},
marks: [
Plot.barY(plotData, {
x: "model",
y: "logLikelihood",
fill: "#1f77b4",
title: d => `${d.model}\nLog-L: ${d.logLikelihood.toFixed(2)}\nAIC: ${d.AIC.toFixed(2)}\nParameters: ${d.parameters}`
})
]
});
}
/**
* Helper function to categorize evidence level based on Bayes Factor
*/
function getEvidenceLevel(bayesFactor) {
if (bayesFactor >= 100)
return "Very Strong";
if (bayesFactor >= 10)
return "Strong";
if (bayesFactor >= 3)
return "Substantial";
if (bayesFactor >= 1)
return "Weak";
return "No Evidence";
}