hyphy-scope
Version:
Reusable Svelte components for HyPhy analysis visualization
308 lines (307 loc) • 11.3 kB
JavaScript
import * as Plot from "@observablehq/plot";
import * as _ from "lodash-es";
import { MEME_COLORS } from "./meme-utils.js";
const DYN_RANGE_CAP = 10000;
export function getMemePlotOptions(hasSiteLRT, hasResamples, bsPositiveSelection) {
return [
{ label: "p-values for selection", available: () => true },
{ label: "p-values for variability", available: () => hasSiteLRT },
{ label: "Site rates", available: () => true },
{ label: "Support for positive selection", available: () => bsPositiveSelection.length > 0 },
{ label: "Dense rate plot", available: () => true },
{ label: "Rate density plots", available: () => true },
{ label: "Q-Q plots", available: () => hasResamples > 0 }
];
}
export function getMemePlotDescription(plotType, hasResamples) {
const descriptions = {
"p-values for selection": `P-values derived from the ${hasResamples ? "parametric bootstrap" : "asymptotic mixture χ²"} test statistic for likelihood ratio tests for episodic diversifying selection. Solid line = user selected significance threshold.`,
"p-values for variability": "P-values derived from the asymptotic mixture χ²₂ test statistic for likelihood ratio tests for variable ω at this site. Solid line = user selected significance threshold.",
"Site rates": `Site-level rate maximum likelihood estimates (MLE). For each site, the horizontal tick depicts the synonymous rate (α) MLE. Circles show non-synonymous (β- and β+) MLEs, and the size of a circle reflects the weight parameter inferred for the corresponding rate. The non-synonymous rate estimates are connected by a vertical line to enhance readability and show the range of inferred non-synonymous rates and their relationship to the synonymous rate. Estimates above ${DYN_RANGE_CAP} are censored at this value.`,
"Dense rate plot": `Maximum likelihood estimates of synonymous (α), non-synonymous rates (β-, β+), and non-synonymous weights (p-,p+) at each site. Estimates above ${DYN_RANGE_CAP} are censored at this value. p-values for episodic diversifying selection are also shown`,
"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.`,
"Support for positive selection": "Empirical Bayes Factors for ω>1 at a particular branch and site (only tested branches are included).",
"Q-Q plots": "Comparison of asymptotic vs bootstrap LRT distributions (limited to 60 sites)."
};
return descriptions[plotType] || "";
}
/**
* Creates a scatter plot for p-values
*/
export function createMemePValuePlot(data, pvalueThreshold) {
return Plot.plot({
width: 800,
height: 300,
marginBottom: 40,
x: {
label: "Codon position",
grid: true
},
y: {
label: "p-value",
type: "log",
grid: true
},
color: {
type: "categorical",
domain: Object.keys(MEME_COLORS),
range: Object.values(MEME_COLORS),
legend: true
},
marks: [
// Significance threshold line
Plot.ruleY([pvalueThreshold], {
stroke: "black",
strokeWidth: 2,
strokeDasharray: "4,2"
}),
// P-value points
Plot.dot(data, {
x: "Codon",
y: "p-value",
fill: "class",
r: 3,
tip: true
})
]
});
}
/**
* Creates a site rates plot showing α, β-, and β+ rates
*/
export function createMemeSiteRatesPlot(data) {
// Prepare data for the complex site rates visualization
const processedData = data.flatMap(d => {
const alpha = Math.min(d.alpha, DYN_RANGE_CAP);
const betaMinus = Math.min(d["beta-"], DYN_RANGE_CAP);
const betaPlus = Math.min(d["beta+"], DYN_RANGE_CAP);
return [
{
codon: d.Codon,
rate: alpha,
type: "alpha",
weight: 1,
class: d.class
},
{
codon: d.Codon,
rate: betaMinus,
type: "beta-",
weight: d["p-"],
class: d.class
},
{
codon: d.Codon,
rate: betaPlus,
type: "beta+",
weight: d["p+"],
class: d.class
}
];
});
return Plot.plot({
width: 800,
height: 400,
marginBottom: 40,
x: {
label: "Codon position",
grid: true
},
y: {
label: "Rate estimate",
type: "sqrt",
grid: true
},
color: {
type: "categorical",
domain: Object.keys(MEME_COLORS),
range: Object.values(MEME_COLORS),
legend: true
},
marks: [
// Alpha rates as horizontal ticks
Plot.tickX(processedData.filter(d => d.type === "alpha"), {
x: "codon",
y: "rate",
stroke: "class",
strokeWidth: 2
}),
// Beta rates as circles sized by weight
Plot.dot(processedData.filter(d => d.type.startsWith("beta")), {
x: "codon",
y: "rate",
fill: "class",
r: d => Math.sqrt(d.weight) * 8,
fillOpacity: 0.7,
tip: true
}),
// Connect beta- and beta+ with lines
Plot.link(data.map(d => ({
codon: d.Codon,
y1: Math.min(d["beta-"], DYN_RANGE_CAP),
y2: Math.min(d["beta+"], DYN_RANGE_CAP),
class: d.class
})), {
x: "codon",
y1: "y1",
y2: "y2",
stroke: "class",
strokeWidth: 1,
strokeOpacity: 0.5
})
]
});
}
/**
* Creates a dense rate plot with multiple rate components
*/
export function createMemeDenseRatePlot(data) {
// Create stacked data for the dense plot
const stackedData = data.flatMap(d => [
{ codon: d.Codon, rate: Math.min(d.alpha, DYN_RANGE_CAP), type: "α", class: d.class, pvalue: d["p-value"] },
{ codon: d.Codon, rate: Math.min(d["beta-"], DYN_RANGE_CAP), type: "β-", class: d.class, pvalue: d["p-value"] },
{ codon: d.Codon, rate: Math.min(d["beta+"], DYN_RANGE_CAP), type: "β+", class: d.class, pvalue: d["p-value"] }
]);
return Plot.plot({
width: 800,
height: 500,
facet: {
data: stackedData,
y: "type",
marginRight: 80
},
x: {
label: "Codon position"
},
y: {
label: "Rate estimate",
grid: true
},
color: {
type: "categorical",
domain: Object.keys(MEME_COLORS),
range: Object.values(MEME_COLORS),
legend: true
},
marks: [
Plot.barY(stackedData, {
x: "codon",
y: "rate",
fill: "class",
fillOpacity: 0.7,
tip: true
})
]
});
}
/**
* Creates rate density plots
*/
export function createMemeRateDensityPlot(data) {
const alphaValues = data.map(d => Math.min(d.alpha, DYN_RANGE_CAP));
const betaMinusValues = data.map(d => Math.min(d["beta-"], DYN_RANGE_CAP));
const betaPlusValues = data.map(d => Math.min(d["beta+"], DYN_RANGE_CAP));
const alphaMean = _.mean(alphaValues);
const betaMinusMean = _.mean(betaMinusValues);
const betaPlusMean = _.mean(betaPlusValues);
// Combine all values for density estimation
const densityData = [
...alphaValues.map(v => ({ value: v, type: "α" })),
...betaMinusValues.map(v => ({ value: v, type: "β-" })),
...betaPlusValues.map(v => ({ value: v, type: "β+" }))
];
return Plot.plot({
width: 600,
height: 400,
x: {
label: "Rate value",
grid: true
},
y: {
label: "Density",
grid: true
},
color: {
domain: ["α", "β-", "β+"],
range: ["steelblue", "orange", "red"]
},
marks: [
// Density curves
Plot.areaY(densityData, Plot.binX({ y: "count" }, {
x: "value",
fill: "type",
fillOpacity: 0.3,
curve: "basis"
})),
// Mean lines
Plot.ruleX([alphaMean], { stroke: "steelblue", strokeWidth: 2, strokeDasharray: "4,2" }),
Plot.ruleX([betaMinusMean], { stroke: "orange", strokeWidth: 2, strokeDasharray: "4,2" }),
Plot.ruleX([betaPlusMean], { stroke: "red", strokeWidth: 2, strokeDasharray: "4,2" }),
// Mean labels
Plot.text([
{ x: alphaMean, y: 0, text: `α: ${alphaMean.toFixed(2)}`, type: "α" },
{ x: betaMinusMean, y: 0, text: `β-: ${betaMinusMean.toFixed(2)}`, type: "β-" },
{ x: betaPlusMean, y: 0, text: `β+: ${betaPlusMean.toFixed(2)}`, type: "β+" }
], {
x: "x",
y: "y",
text: "text",
fill: d => d.type === "α" ? "steelblue" : d.type === "β-" ? "orange" : "red",
dy: -10,
fontSize: 12
})
]
});
}
/**
* Creates support for positive selection plot (Bayes factors)
*/
export function createMemeSupportPlot(bsData) {
if (!bsData || bsData.length === 0) {
return null;
}
return Plot.plot({
width: 800,
height: 400,
x: {
label: "Codon position",
grid: true
},
y: {
label: "Empirical Bayes Factor",
type: "log",
grid: true
},
marks: [
Plot.dot(bsData, {
x: "Codon",
y: "ER",
fill: "Branch",
r: 3,
fillOpacity: 0.7,
tip: true
}),
// Reference line at ER = 1
Plot.ruleY([1], { stroke: "black", strokeDasharray: "2,2" })
]
});
}
/**
* Main function to create the appropriate plot based on type
*/
export function createMemePlot(plotType, data, pvalueThreshold, bsData) {
switch (plotType) {
case "p-values for selection":
return createMemePValuePlot(data, pvalueThreshold);
case "Site rates":
return createMemeSiteRatesPlot(data);
case "Dense rate plot":
return createMemeDenseRatePlot(data);
case "Rate density plots":
return createMemeRateDensityPlot(data);
case "Support for positive selection":
return createMemeSupportPlot(bsData || []);
default:
return null;
}
}