hyphy-scope
Version:
Reusable Svelte components for HyPhy analysis visualization
238 lines (237 loc) • 7.79 kB
JavaScript
/**
* Multi-Hit utility functions
*/
// Genetic code translation table (codon -> amino acid)
export const translationTable = {
TTT: 'F', TTC: 'F', TTA: 'L', TTG: 'L',
TCT: 'S', TCC: 'S', TCA: 'S', TCG: 'S',
TAT: 'Y', TAC: 'Y', TAA: '*', TAG: '*',
TGT: 'C', TGC: 'C', TGA: '*', TGG: 'W',
CTT: 'L', CTC: 'L', CTA: 'L', CTG: 'L',
CCT: 'P', CCC: 'P', CCA: 'P', CCG: 'P',
CAT: 'H', CAC: 'H', CAA: 'Q', CAG: 'Q',
CGT: 'R', CGC: 'R', CGA: 'R', CGG: 'R',
ATT: 'I', ATC: 'I', ATA: 'I', ATG: 'M',
ACT: 'T', ACC: 'T', ACA: 'T', ACG: 'T',
AAT: 'N', AAC: 'N', AAA: 'K', AAG: 'K',
AGT: 'S', AGC: 'S', AGA: 'R', AGG: 'R',
GTT: 'V', GTC: 'V', GTA: 'V', GTG: 'V',
GCT: 'A', GCC: 'A', GCA: 'A', GCG: 'A',
GAT: 'D', GAC: 'D', GAA: 'E', GAG: 'E',
GGT: 'G', GGC: 'G', GGA: 'G', GGG: 'G'
};
/**
* Get unique amino acids in order
*/
export function getUniqueAminoAcids() {
const aminoAcids = Object.values(translationTable);
return Array.from(new Set(aminoAcids)).sort();
}
/**
* Get amino acid index for color mapping
*/
export function getAminoAcidIndex(aa) {
const uniqueAAs = getUniqueAminoAcids();
return uniqueAAs.indexOf(aa) / uniqueAAs.length;
}
/**
* Format evidence ratio data for table display
*/
export function formatEvidenceRatios(data) {
const erKeys = Object.keys(data['Evidence Ratios']);
const erValues = erKeys.map(key => data['Evidence Ratios'][key][0]);
if (erValues.length === 0)
return [];
const numSites = erValues[0].length;
const rows = [];
for (let i = 0; i < numSites; i++) {
const row = [i + 1]; // Site number
erKeys.forEach((key, idx) => {
row.push(erValues[idx][i]);
});
rows.push(row);
}
return rows;
}
/**
* Format site log likelihood data for table display
*/
export function formatSiteLogLikelihood(data) {
const logLKeys = Object.keys(data['Site Log Likelihood']);
const logLValues = logLKeys.map(key => data['Site Log Likelihood'][key][0]);
if (logLValues.length === 0)
return [];
const numSites = logLValues[0].length;
const rows = [];
for (let i = 0; i < numSites; i++) {
const row = [i + 1]; // Site number
logLKeys.forEach((key, idx) => {
row.push(logLValues[idx][i]);
});
rows.push(row);
}
return rows;
}
/**
* Get ER range (min, max) for a dataset
*/
/**
* Format number to 3 significant figures
*/
function formatSigFigs(num, sigFigs = 3) {
if (num === 0)
return 0;
const magnitude = Math.floor(Math.log10(Math.abs(num)));
const scale = Math.pow(10, sigFigs - magnitude - 1);
return Math.round(num * scale) / scale;
}
export function getERRange(values) {
const min = Math.min(...values);
const max = Math.max(...values);
return [formatSigFigs(min, 3), formatSigFigs(max, 3)];
}
/**
* Initialize ER thresholds from data
*/
export function initializeERThresholds(data) {
const thresholds = {};
Object.entries(data['Evidence Ratios']).forEach(([key, values]) => {
const range = getERRange(values[0]);
thresholds[key] = range;
});
return thresholds;
}
/**
* Count nucleotide differences between two codons
*/
export function countNucleotideDifferences(codon1, codon2) {
let count = 0;
for (let i = 0; i < 3; i++) {
if (codon1[i] !== codon2[i])
count++;
}
return count;
}
/**
* Prepare codon substitution data for circos visualization
*/
export function prepareCircosData(substitutions, evidenceRatios, erThresholds, minTransitions) {
// Filter sites based on ER thresholds
const erKeys = Object.keys(evidenceRatios);
const sitesToKeep = new Set();
if (erKeys.length > 0) {
const numSites = evidenceRatios[erKeys[0]][0].length;
for (let site = 0; site < numSites; site++) {
let keepSite = true;
for (const key of erKeys) {
const er = evidenceRatios[key][0][site];
const [min, max] = erThresholds[key] || [0, Infinity];
if (er < min || er > max) {
keepSite = false;
break;
}
}
if (keepSite) {
sitesToKeep.add(site + 1);
}
}
}
// Count transitions
const counts = {};
Object.entries(substitutions).forEach(([siteStr, sources]) => {
const siteNum = parseInt(siteStr);
if (!sitesToKeep.has(siteNum))
return;
Object.entries(sources).forEach(([sourceCodon, targets]) => {
if (!counts[sourceCodon])
counts[sourceCodon] = {};
Object.entries(targets).forEach(([targetCodon, occurrences]) => {
if (!counts[sourceCodon][targetCodon]) {
counts[sourceCodon][targetCodon] = 0;
}
counts[sourceCodon][targetCodon] += Object.keys(occurrences).length;
});
});
});
// Build chord data
const chords = [];
const codonTally = {};
let maxCount = 0;
// Initialize tally for all codons
Object.keys(translationTable).forEach(codon => {
codonTally[codon] = 0;
});
Object.entries(counts).forEach(([sourceCodon, targets]) => {
Object.entries(targets).forEach(([targetCodon, count]) => {
if (count > maxCount)
maxCount = count;
const nucDiff = countNucleotideDifferences(sourceCodon, targetCodon);
if (nucDiff >= minTransitions) {
chords.push({
count,
source: {
id: `codon-${sourceCodon}`,
start: codonTally[sourceCodon],
end: codonTally[sourceCodon] += count,
codon: sourceCodon
},
target: {
id: `codon-${targetCodon}`,
start: codonTally[targetCodon],
end: codonTally[targetCodon] += count,
codon: targetCodon
}
});
}
});
});
// Get unique codons from chords
const uniqueCodons = new Set();
chords.forEach(chord => {
uniqueCodons.add(chord.source.codon);
uniqueCodons.add(chord.target.codon);
});
// Build layout
const layout = Array.from(uniqueCodons).map(codon => ({
len: codonTally[codon],
color: getCodonColor(codon),
label: codon,
id: `codon-${codon}`,
aa: translationTable[codon] || '?'
}));
return { layout, chords, maxCount };
}
/**
* Get color for a codon based on its amino acid
*/
export function getCodonColor(codon) {
const aa = translationTable[codon];
if (!aa)
return '#999';
const uniqueAAs = getUniqueAminoAcids();
const index = uniqueAAs.indexOf(aa);
// Use d3 category20 colors
const colors = [
'#1f77b4', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c',
'#98df8a', '#d62728', '#ff9896', '#9467bd', '#c5b0d5',
'#8c564b', '#c49c94', '#e377c2', '#f7b6d2', '#7f7f7f',
'#c7c7c7', '#bcbd22', '#dbdb8d', '#17becf', '#9edae5'
];
return colors[index % colors.length];
}
/**
* Get test result summary
*/
export function getTestSummary(data, pThreshold = 0.1) {
const testResults = data['test results'];
const significant = [];
Object.entries(testResults).forEach(([testName, result]) => {
if (result && result['p-value'] < pThreshold) {
significant.push(testName);
}
});
return {
significant,
total: Object.keys(testResults).length
};
}