hyphy-scope
Version:
Reusable Svelte components for HyPhy analysis visualization
294 lines (293 loc) • 11 kB
JavaScript
import * as Plot from "@observablehq/plot";
import * as _ from "lodash-es";
// Colors and types moved to FEL component
const COLORS = {
'Diversifying': '#e74c3c',
'Neutral': '#95a5a6',
'Purifying': '#3498db',
'Invariable': '#ecf0f1'
};
const DYN_RANGE_CAP = 10;
export function getFelPlotOptions(hasPasmt) {
return [
{ label: "Site-level dN/dS estimates", available: (d) => !!d["confidence interval"] },
{ label: "alpha/beta site-level estimates", available: () => true },
{ label: "Bootstrap vs asymptotic p-value", available: () => hasPasmt },
{ label: "Rate density plots", available: () => true },
{ label: "Q-Q plots", available: () => hasPasmt },
{ label: "Dense rate plot", available: () => true }
];
}
export function getFelPlotDescription(plotType, pvalueThreshold) {
const descriptions = {
"Site-level dN/dS estimates": "Maximum likelihood estimates of dN/dS at each site, together with estimated profile confidence intervals (if available). dN/dS = 1 (neutrality) is depicted as a horizontal gray line. Boundaries between partitions (if present) are shown as vertical dashed lines.",
"alpha/beta site-level estimates": `Maximum likelihood estimates of synonymous (α) and non-synonymous rates (β) at each site shown as bars. The line shows the estimates under the null model (α=β). Estimates above ${DYN_RANGE_CAP} are censored at this value.`,
"Dense rate plot": `Maximum likelihood estimates of synonymous (α) and non-synonymous rates (β) at each site. Estimates above ${DYN_RANGE_CAP} are censored at this value. p-values are also shown`,
"Bootstrap vs asymptotic p-value": `Comparison of site-level p-values for non-neutrality using parametric bootstrap and the asymptotic approximation. Rejection region (p ≤ ${pvalueThreshold}) is shown as a shaded rectangle`,
"Rate density plots": `Kernel density estimates of site-level rate estimates. Means are shown with red rules. Estimates above ${DYN_RANGE_CAP} are censored at this value.`,
"Q-Q plots": "Comparison of asymptotic vs bootstrap LRT distributions (limited to 60 sites)."
};
return descriptions[plotType] || "";
}
/**
* Creates an Observable Plot for dN/dS estimates with confidence intervals
*/
export function createDnDsPlot(data) {
// Cap values at DYN_RANGE_CAP
const processedData = data.map(d => ({
...d,
"dN/dS MLE": Math.min(d["dN/dS MLE"] || 0, DYN_RANGE_CAP),
"dN/dS LB": Math.min(d["dN/dS LB"] || 0, DYN_RANGE_CAP),
"dN/dS UB": Math.min(d["dN/dS UB"] || 0, DYN_RANGE_CAP)
}));
return Plot.plot({
width: 800,
height: 400,
marginBottom: 40,
x: {
label: "Codon",
tickFormat: d => d.toString()
},
y: {
label: "dN/dS",
grid: true,
type: "sqrt"
},
color: {
type: "categorical",
domain: Object.keys(COLORS),
range: Object.values(COLORS),
legend: true
},
marks: [
// Neutral line at y=1
Plot.ruleY([1], { stroke: "gray", strokeOpacity: 0.5, strokeWidth: 2 }),
// Confidence intervals (if available)
...(data[0]?.["dN/dS LB"] !== undefined ? [
Plot.link(processedData, {
x: "codon",
y1: "dN/dS LB",
y2: "dN/dS UB",
stroke: "steelblue",
strokeWidth: 1
})
] : []),
// Point estimates
Plot.dot(processedData, {
x: "codon",
y: "dN/dS MLE",
fill: "class",
r: 4,
tip: true
})
]
});
}
/**
* Creates an Observable Plot for alpha/beta rate estimates matching the original FEL visualization
*/
export function createAlphaBetaPlot(data) {
// Process data and cap values
const processedData = data.map(d => ({
codon: d.codon,
alpha: -Math.min(d.alpha, DYN_RANGE_CAP), // Negative for downward bars
beta: Math.min(d.beta, DYN_RANGE_CAP), // Positive for upward bars
neutral: Math.min(d["alpha=beta"], DYN_RANGE_CAP),
class: d.class,
partition: d.partition
}));
const yMax = Math.max(...processedData.map(d => Math.max(Math.abs(d.alpha), d.beta)));
return Plot.plot({
width: 800,
height: 400,
marginLeft: 60,
marginBottom: 50,
x: {
label: "Codon",
tickFormat: d => d.toString()
},
y: {
label: "Rate estimate",
domain: [-yMax * 1.1, yMax * 1.1],
grid: true,
tickFormat: d => {
const absVal = Math.abs(d);
if (d > 0)
return `β = ${absVal.toFixed(1)}`;
if (d < 0)
return `α = ${absVal.toFixed(1)}`;
return "0";
}
},
color: {
type: "categorical",
domain: Object.keys(COLORS),
range: Object.values(COLORS)
},
marks: [
// Zero reference line
Plot.ruleY([0], {
stroke: "gray",
strokeOpacity: 0.5,
strokeWidth: 1
}),
// Alpha bars (negative, extending downward)
Plot.rect(processedData, {
x: d => d.codon - 0.4,
x2: d => d.codon + 0.4,
y: 0,
y2: "alpha",
fill: "class",
stroke: "white",
strokeWidth: 0.5,
fillOpacity: 0.8
}),
// Beta bars (positive, extending upward)
Plot.rect(processedData, {
x: d => d.codon - 0.4,
x2: d => d.codon + 0.4,
y: 0,
y2: "beta",
fill: "class",
stroke: "white",
strokeWidth: 0.5,
fillOpacity: 0.8
}),
// Neutral rate horizontal lines (connecting alpha=beta estimates)
Plot.link(processedData, {
x: d => d.codon - 0.4,
x2: d => d.codon + 0.4,
y: "neutral",
y2: "neutral",
stroke: "#333",
strokeWidth: 2,
strokeOpacity: 0.8
}),
// Add tooltips with invisible rects
Plot.rect(processedData, {
x: d => d.codon - 0.4,
x2: d => d.codon + 0.4,
y: "alpha",
y2: "beta",
fill: "transparent",
title: d => `Codon ${d.codon}\nα = ${Math.abs(d.alpha).toFixed(3)}\nβ = ${d.beta.toFixed(3)}\nα=β = ${d.neutral.toFixed(3)}\nClass: ${d.class}`
})
]
});
}
/**
* Creates an Observable Plot for p-value comparison
*/
export function createPValuePlot(data, pvalueThreshold) {
const processedData = data.map(d => ({
...d,
agree: ((d["p-value"] - pvalueThreshold) * ((d["p-asmp"] || 0) - pvalueThreshold) >= 0) ? "Yes" : "No"
}));
return Plot.plot({
width: 500,
height: 500,
x: {
label: "Bootstrap p-value",
type: "sqrt",
grid: true
},
y: {
label: "Asymptotic p-value",
type: "sqrt",
grid: true
},
color: {
type: "categorical",
domain: Object.keys(COLORS),
range: Object.values(COLORS),
legend: true
},
marks: [
// Rejection region
Plot.rect([{ x1: 0, y1: 0, x2: pvalueThreshold, y2: pvalueThreshold }], {
x1: "x1",
y1: "y1",
x2: "x2",
y2: "y2",
fill: "#DDD",
fillOpacity: 0.3
}),
// Points
Plot.dot(processedData, {
x: "p-value",
y: "p-asmp",
fill: "class",
symbol: d => d.agree === "Yes" ? "circle" : "cross",
r: 5,
fillOpacity: 0.7,
tip: true
})
]
});
}
/**
* Creates a rate density plot
*/
export function createRateDensityPlot(data) {
const alphaValues = data.map(d => Math.min(d.alpha, DYN_RANGE_CAP));
const betaValues = data.map(d => Math.min(d.beta, DYN_RANGE_CAP));
const alphaMean = _.mean(alphaValues);
const betaMean = _.mean(betaValues);
return Plot.plot({
width: 600,
height: 400,
x: {
label: "Rate value",
grid: true
},
y: {
label: "Density",
grid: true
},
marks: [
// Alpha density
Plot.areaY(alphaValues, Plot.binX({ y: "count" }, { x: d => d }), { fill: "steelblue", fillOpacity: 0.3, curve: "basis" }),
Plot.lineY(alphaValues, Plot.binX({ y: "count" }, { x: d => d }), { stroke: "steelblue", strokeWidth: 2, curve: "basis" }),
// Beta density
Plot.areaY(betaValues, Plot.binX({ y: "count" }, { x: d => d }), { fill: "orange", fillOpacity: 0.3, curve: "basis" }),
Plot.lineY(betaValues, Plot.binX({ y: "count" }, { x: d => d }), { stroke: "orange", strokeWidth: 2, curve: "basis" }),
// Mean lines
Plot.ruleX([alphaMean], { stroke: "steelblue", strokeWidth: 2, strokeDasharray: "4,2" }),
Plot.ruleX([betaMean], { stroke: "orange", strokeWidth: 2, strokeDasharray: "4,2" }),
// Labels
Plot.text([{ x: alphaMean, y: 0, text: `α mean: ${alphaMean.toFixed(2)}` }], {
x: "x",
y: "y",
text: "text",
dy: -10,
fill: "steelblue",
fontSize: 12
}),
Plot.text([{ x: betaMean, y: 0, text: `β mean: ${betaMean.toFixed(2)}` }], {
x: "x",
y: "y",
text: "text",
dy: -25,
fill: "orange",
fontSize: 12
})
]
});
}
/**
* Main function to create the appropriate plot based on type
*/
export function createFelPlot(plotType, data, pvalueThreshold) {
switch (plotType) {
case "Site-level dN/dS estimates":
return createDnDsPlot(data);
case "alpha/beta site-level estimates":
return createAlphaBetaPlot(data);
case "Bootstrap vs asymptotic p-value":
return createPValuePlot(data, pvalueThreshold);
case "Rate density plots":
return createRateDensityPlot(data);
default:
return null;
}
}