← Files Biological Sequence & Alignment ViewerARCHIVED FILE
src/msa/colors.ts
22.8 KB · Sep 30, 2026 · 23:01 UTC
import { getSequenceResidueStyle } from "../sequence/sequence-palette";
import type { MsaColumnSummary } from "./conservation";
import { classifyResidue, isGapSymbol } from "./residue-alphabet";
import type { MsaDocument, MsaMoleculeType } from "./types";
export type MsaColorMode =
| "coding-impact"
| "difference"
| "identity"
| "nucleotide-substitution"
| "protein-conservation"
| "protein-similarity"
| "residue";
export type MsaProteinPalette =
"clustal-x" | "hydrophobicity" | "muted-amino-acid" | "rasmol" | "zappo";
export type MsaNucleicAcidPalette =
| "jalview-nucleotide"
| "muted-nucleic-acid"
| "ncbi-nucleic-acid"
| "nucleotide-ambiguity"
| "purine-pyrimidine";
export type MsaResiduePalette =
MsaNucleicAcidPalette | MsaProteinPalette | "neutral";
const PROTEIN_RESIDUE_PALETTES: Array<MsaResiduePalette> = [
"muted-amino-acid",
"neutral",
"rasmol",
"clustal-x",
"zappo",
"hydrophobicity",
];
const NUCLEIC_ACID_RESIDUE_PALETTES: Array<MsaResiduePalette> = [
"muted-nucleic-acid",
"neutral",
"ncbi-nucleic-acid",
"jalview-nucleotide",
"purine-pyrimidine",
"nucleotide-ambiguity",
];
export const DIFFERENCE_MATCH_COLOR = "#f8fafc";
export const DIFFERENCE_MISMATCH_COLOR = "#ffd6a8";
export const CODING_SYNONYMOUS_COLOR = "#bfdbfe";
export const CODING_NONSYNONYMOUS_COLOR = "#fecaca";
export const NUCLEOTIDE_TRANSITION_COLOR = "#fde68a";
export const NUCLEOTIDE_TRANSVERSION_COLOR = "#ddd6fe";
const UNKNOWN_RESIDUE_COLOR = "#f1f3f5";
export const DARK_PALETTE_TEXT_COLOR = "#ffffff";
export const LIGHT_PALETTE_TEXT_COLOR = "#111827";
const identityLowColor = "#eff6ff";
const identityHighColor = "#60a5fa";
const proteinConservationLowColor = "#dbeafe";
const proteinConservationHighColor = "#fca5a5";
const proteinSimilarityLowColor = "#f97316";
const proteinSimilarityNeutralColor = DIFFERENCE_MATCH_COLOR;
const proteinSimilarityHighColor = "#2563eb";
const rasMolColors: Record<string, string> = {
A: "#c8c8c8",
C: "#e6e600",
D: "#e60a0a",
E: "#e60a0a",
F: "#3232aa",
G: "#ebebeb",
H: "#8282d2",
I: "#0f820f",
K: "#145aff",
L: "#0f820f",
M: "#e6e600",
N: "#00dcdc",
P: "#dc9682",
Q: "#00dcdc",
R: "#145aff",
S: "#fa9600",
T: "#fa9600",
V: "#0f820f",
W: "#b45ab4",
Y: "#3232aa",
};
const zappoColors = {
aliphaticHydrophobic: "#ffafaf",
aromatic: "#ffc800",
conformationallySpecial: "#ff00ff",
cysteine: "#ffff00",
hydrophilic: "#00ff00",
negative: "#ff0000",
positive: "#6464ff",
};
const clustalColors = {
aromatic: "#15a4a4",
cysteine: "#f08080",
glycine: "#f09048",
hydrophobic: "#80a0f0",
negative: "#c048c0",
polar: "#15c015",
positive: "#f01505",
proline: "#c0c000",
};
const hydrophobicityColors: Record<string, string> = {
A: "#ad0052",
B: "#0c00f3",
C: "#c2003d",
D: "#0c00f3",
E: "#0c00f3",
F: "#cb0034",
G: "#6a0095",
H: "#1500ea",
I: "#ff0000",
K: "#0000ff",
L: "#ea0015",
M: "#b0004f",
N: "#0c00f3",
P: "#4600b9",
Q: "#0c00f3",
R: "#0000ff",
S: "#5e00a1",
T: "#61009e",
V: "#f60009",
W: "#5b00a4",
X: "#680097",
Y: "#4f00b0",
Z: "#0c00f3",
};
const ncbiNucleotideColors: Record<string, string> = {
A: "#ff0000",
C: "#ffff00",
G: "#0000ff",
T: "#008000",
U: "#008000",
};
const jalviewNucleotideColors: Record<string, string> = {
A: "#64f73f",
C: "#ffb340",
G: "#eb413c",
T: "#3c88ee",
U: "#3c88ee",
};
const purinePyrimidineColors: Record<string, string> = {
A: "#ff83fa",
C: "#40e0d0",
G: "#ff83fa",
R: "#ff83fa",
T: "#40e0d0",
U: "#40e0d0",
Y: "#40e0d0",
};
const nucleotideAmbiguityColors: Record<string, string> = {
A: "#f0fff0",
B: "#8b4513",
C: "#f0fff0",
D: "#483d8b",
G: "#f0fff0",
H: "#808080",
I: "#ffffff",
K: "#9932cc",
M: "#9acd32",
N: "#2f4f4f",
R: "#cd5c5c",
S: "#ff8c00",
T: "#f0fff0",
U: "#f0fff0",
V: "#b8860b",
W: "#4682b4",
X: "#4f6f6f",
Y: "#008000",
};
const rasMolFallbackColor = "#bea06e";
export function getDefaultColorMode(
moleculeType: MsaMoleculeType,
): MsaColorMode {
return moleculeType === "dna" ||
moleculeType === "rna" ||
moleculeType === "nucleic-acid-ambiguous"
? "difference"
: "residue";
}
export function getDefaultResiduePalette(
moleculeType: MsaMoleculeType,
): MsaResiduePalette | null {
if (moleculeType === "protein") {
return "muted-amino-acid";
}
if (
moleculeType === "dna" ||
moleculeType === "rna" ||
moleculeType === "nucleic-acid-ambiguous"
) {
return "muted-nucleic-acid";
}
return "neutral";
}
export function getCompatibleResiduePalettes(
moleculeType: MsaMoleculeType,
): Array<MsaResiduePalette> {
if (moleculeType === "protein") {
return PROTEIN_RESIDUE_PALETTES;
}
if (
moleculeType === "dna" ||
moleculeType === "rna" ||
moleculeType === "nucleic-acid-ambiguous"
) {
return NUCLEIC_ACID_RESIDUE_PALETTES;
}
return ["neutral"];
}
export function isThemeAwareMsaPalette(
palette: MsaResiduePalette | null,
): palette is "muted-amino-acid" | "muted-nucleic-acid" | "neutral" {
return (
palette === "muted-amino-acid" ||
palette === "muted-nucleic-acid" ||
palette === "neutral"
);
}
export function isResiduePaletteCompatible(
moleculeType: MsaMoleculeType,
palette: MsaResiduePalette | null,
): palette is MsaResiduePalette {
return (
palette != null &&
getCompatibleResiduePalettes(moleculeType).includes(palette)
);
}
function getIdentityColor(identity: number): string {
return interpolateHex(identityLowColor, identityHighColor, identity);
}
function getProteinConservationColor(
conservationNormalized: number | null,
): string {
return interpolateHex(
proteinConservationLowColor,
proteinConservationHighColor,
conservationNormalized ?? 0,
);
}
function getProteinSimilarityColor(score: number | null): string {
if (score == null) {
return proteinSimilarityNeutralColor;
}
if (score <= 0) {
return interpolateHex(
proteinSimilarityLowColor,
proteinSimilarityNeutralColor,
(score + 4) / 4,
);
}
return interpolateHex(
proteinSimilarityNeutralColor,
proteinSimilarityHighColor,
score / 11,
);
}
function getCountFraction(
summary: MsaColumnSummary,
symbols: ReadonlyArray<string>,
): number {
if (summary.nongapCount === 0) {
return 0;
}
const count = symbols.reduce(
(total, symbol) => total + (summary.symbolCounts[symbol] ?? 0),
0,
);
return count / summary.nongapCount;
}
function matchesClustalRule({
summary,
threshold,
symbols,
}: {
summary: MsaColumnSummary;
threshold: number;
symbols: ReadonlyArray<string>;
}): boolean {
return getCountFraction(summary, symbols) > threshold;
}
function getClustalXColor(symbol: string, summary: MsaColumnSummary): string {
const normalized = symbol.toUpperCase();
const hydrophobicGroup = [
"W",
"L",
"V",
"I",
"M",
"A",
"F",
"C",
"Y",
"H",
"P",
];
const aromaticConsensus = [
"W",
"Y",
"A",
"C",
"P",
"Q",
"F",
"H",
"I",
"L",
"M",
"V",
];
if (
normalized === "C" &&
matchesClustalRule({ summary, symbols: ["C"], threshold: 0.85 })
) {
return clustalColors.cysteine;
}
if (normalized === "G") {
return clustalColors.glycine;
}
if (normalized === "P") {
return clustalColors.proline;
}
if (
["H", "Y"].includes(normalized) &&
(matchesClustalRule({
summary,
symbols: hydrophobicGroup,
threshold: 0.6,
}) ||
matchesClustalRule({
summary,
symbols: aromaticConsensus,
threshold: 0.85,
}))
) {
return clustalColors.aromatic;
}
if (
["K", "R"].includes(normalized) &&
(matchesClustalRule({ summary, symbols: ["K", "R"], threshold: 0.6 }) ||
matchesClustalRule({
summary,
symbols: ["K", "R", "Q"],
threshold: 0.85,
}))
) {
return clustalColors.positive;
}
if (
normalized === "E" &&
(matchesClustalRule({ summary, symbols: ["K", "R"], threshold: 0.6 }) ||
matchesClustalRule({ summary, symbols: ["Q", "E"], threshold: 0.5 }) ||
matchesClustalRule({ summary, symbols: ["E", "D"], threshold: 0.5 }) ||
matchesClustalRule({
summary,
symbols: ["E", "Q", "D"],
threshold: 0.85,
}))
) {
return clustalColors.negative;
}
if (
normalized === "D" &&
(matchesClustalRule({ summary, symbols: ["K", "R"], threshold: 0.6 }) ||
matchesClustalRule({
summary,
symbols: ["D", "E", "N"],
threshold: 0.85,
}) ||
matchesClustalRule({ summary, symbols: ["E", "D"], threshold: 0.5 }))
) {
return clustalColors.negative;
}
if (
normalized === "N" &&
(matchesClustalRule({ summary, symbols: ["N"], threshold: 0.5 }) ||
matchesClustalRule({ summary, symbols: ["N", "D"], threshold: 0.85 }))
) {
return clustalColors.polar;
}
if (
normalized === "Q" &&
(matchesClustalRule({ summary, symbols: ["K", "R"], threshold: 0.6 }) ||
matchesClustalRule({ summary, symbols: ["Q", "E"], threshold: 0.5 }) ||
matchesClustalRule({
summary,
symbols: ["Q", "T", "K", "R"],
threshold: 0.85,
}))
) {
return clustalColors.polar;
}
if (
["S", "T"].includes(normalized) &&
(matchesClustalRule({
summary,
symbols: hydrophobicGroup,
threshold: 0.6,
}) ||
matchesClustalRule({ summary, symbols: ["T", "S"], threshold: 0.5 }) ||
matchesClustalRule({ summary, symbols: ["S", "T"], threshold: 0.85 }))
) {
return clustalColors.polar;
}
if (
["A", "C", "I", "L", "M", "F", "W", "V"].includes(normalized) &&
matchesClustalRule({
summary,
symbols: hydrophobicGroup,
threshold: 0.6,
})
) {
return clustalColors.hydrophobic;
}
return "transparent";
}
function getZappoColor(symbol: string): string {
const normalized = symbol.toUpperCase();
if (["I", "L", "V", "A", "M"].includes(normalized)) {
return zappoColors.aliphaticHydrophobic;
}
if (["F", "W", "Y"].includes(normalized)) {
return zappoColors.aromatic;
}
if (["K", "R", "H"].includes(normalized)) {
return zappoColors.positive;
}
if (["D", "E"].includes(normalized)) {
return zappoColors.negative;
}
if (["S", "T", "N", "Q"].includes(normalized)) {
return zappoColors.hydrophilic;
}
if (["P", "G"].includes(normalized)) {
return zappoColors.conformationallySpecial;
}
if (normalized === "C") {
return zappoColors.cysteine;
}
return UNKNOWN_RESIDUE_COLOR;
}
function getResidueModeColor({
columnSummary,
moleculeType,
palette,
symbol,
}: {
columnSummary: MsaColumnSummary;
moleculeType: MsaMoleculeType;
palette: MsaResiduePalette | null;
symbol: string;
}): string {
if (isThemeAwareMsaPalette(palette)) {
if (!isResiduePaletteCompatible(moleculeType, palette)) {
return UNKNOWN_RESIDUE_COLOR;
}
return getSequenceResidueStyle({
molecule: moleculeType === "mixed" ? "unknown" : moleculeType,
paletteId: palette,
residue: symbol,
}).backgroundColor;
}
if (isGapSymbol(symbol)) {
return "transparent";
}
const normalized = symbol.toUpperCase();
if (moleculeType === "protein") {
switch (palette) {
case "rasmol":
return rasMolColors[normalized] ?? rasMolFallbackColor;
case "clustal-x":
return getClustalXColor(normalized, columnSummary);
case "zappo":
return getZappoColor(normalized);
case "hydrophobicity":
return hydrophobicityColors[normalized] ?? UNKNOWN_RESIDUE_COLOR;
case "ncbi-nucleic-acid":
case "jalview-nucleotide":
case "purine-pyrimidine":
case "nucleotide-ambiguity":
case null:
return UNKNOWN_RESIDUE_COLOR;
}
}
if (
moleculeType === "dna" ||
moleculeType === "rna" ||
moleculeType === "nucleic-acid-ambiguous"
) {
switch (palette) {
case "ncbi-nucleic-acid":
return ncbiNucleotideColors[normalized] ?? UNKNOWN_RESIDUE_COLOR;
case "jalview-nucleotide":
return jalviewNucleotideColors[normalized] ?? UNKNOWN_RESIDUE_COLOR;
case "purine-pyrimidine":
return purinePyrimidineColors[normalized] ?? UNKNOWN_RESIDUE_COLOR;
case "nucleotide-ambiguity":
return nucleotideAmbiguityColors[normalized] ?? UNKNOWN_RESIDUE_COLOR;
case "rasmol":
case "clustal-x":
case "zappo":
case "hydrophobicity":
case null:
return UNKNOWN_RESIDUE_COLOR;
}
}
const residueClass = classifyResidue(symbol, moleculeType);
return residueClass === "unknown" ? UNKNOWN_RESIDUE_COLOR : "#e8eef8";
}
export function getMsaCellBackground({
columnSummary,
document,
mode,
palette,
referenceSymbol,
symbol,
}: {
columnSummary: MsaColumnSummary;
document: MsaDocument;
mode: MsaColorMode;
palette: MsaResiduePalette | null;
referenceSymbol?: string;
symbol: string;
}): string {
if (mode === "identity") {
return getIdentityColor(columnSummary.identity);
}
if (mode === "protein-conservation") {
return getProteinConservationColor(columnSummary.conservationNormalized);
}
if (mode === "protein-similarity") {
return getProteinSimilarityColor(
getProteinSimilarityScore({
columnSummary,
referenceSymbol,
symbol,
}),
);
}
if (mode === "nucleotide-substitution") {
return getNucleotideSubstitutionColor({ referenceSymbol, symbol });
}
if (mode === "coding-impact") {
return isGapSymbol(symbol) ? "transparent" : DIFFERENCE_MATCH_COLOR;
}
if (mode === "difference") {
if (referenceSymbol == null) {
return isGapSymbol(symbol) ? "transparent" : DIFFERENCE_MATCH_COLOR;
}
if (symbol.toUpperCase() !== referenceSymbol.toUpperCase()) {
return DIFFERENCE_MISMATCH_COLOR;
}
return isGapSymbol(symbol) ? "transparent" : DIFFERENCE_MATCH_COLOR;
}
return getResidueModeColor({
columnSummary,
moleculeType: document.displayInterpretation.moleculeType,
palette,
symbol,
});
}
export function getNucleotideSubstitutionColor({
referenceSymbol,
symbol,
}: {
referenceSymbol?: string;
symbol: string;
}): string {
if (referenceSymbol == null) {
return isGapSymbol(symbol) ? "transparent" : DIFFERENCE_MATCH_COLOR;
}
const reference = normalizeNucleotideBase(referenceSymbol);
const candidate = normalizeNucleotideBase(symbol);
if (candidate === reference) {
return isGapSymbol(symbol) ? "transparent" : DIFFERENCE_MATCH_COLOR;
}
if (
!["A", "C", "G", "T"].includes(reference) ||
!["A", "C", "G", "T"].includes(candidate)
) {
return DIFFERENCE_MISMATCH_COLOR;
}
const transition =
(reference === "A" && candidate === "G") ||
(reference === "G" && candidate === "A") ||
(reference === "C" && candidate === "T") ||
(reference === "T" && candidate === "C");
return transition
? NUCLEOTIDE_TRANSITION_COLOR
: NUCLEOTIDE_TRANSVERSION_COLOR;
}
function normalizeNucleotideBase(symbol: string): string {
const normalized = symbol.toUpperCase();
return normalized === "U" ? "T" : normalized;
}
export function getMsaCellTextColor({
backgroundColor,
mode,
moleculeType,
palette,
symbol,
}: {
backgroundColor: string;
mode: MsaColorMode;
moleculeType: MsaMoleculeType;
palette: MsaResiduePalette | null;
symbol: string;
}): string | undefined {
if (
mode === "residue" &&
isThemeAwareMsaPalette(palette) &&
isResiduePaletteCompatible(moleculeType, palette)
) {
return getSequenceResidueStyle({
molecule: moleculeType === "mixed" ? "unknown" : moleculeType,
paletteId: palette,
residue: symbol,
}).color;
}
return getReadableMsaCellTextColor(backgroundColor);
}
export function getReadableMsaCellTextColor(
backgroundColor: string,
): string | undefined {
const backgroundRgb = parseOpaqueHexColor(backgroundColor);
if (backgroundRgb == null) {
return undefined;
}
const backgroundLuminance = getRelativeLuminance(backgroundRgb);
const darkTextLuminance = getRelativeLuminance([17, 24, 39]);
const lightTextLuminance = 1;
const darkTextContrast = getContrastRatio(
backgroundLuminance,
darkTextLuminance,
);
const lightTextContrast = getContrastRatio(
backgroundLuminance,
lightTextLuminance,
);
return lightTextContrast > darkTextContrast
? DARK_PALETTE_TEXT_COLOR
: LIGHT_PALETTE_TEXT_COLOR;
}
export function getLegendSwatchColor({
palette,
symbol,
}: {
palette: MsaResiduePalette;
symbol: string;
}): string {
const summary: MsaColumnSummary = {
conservationModel: null,
conservationNormalized: null,
gapFraction: 0,
identity: 1,
nongapCount: 1,
nucleotideInformationContentBits: null,
proteinRelativeEntropyBits: null,
symbolCounts: { [symbol.toUpperCase()]: 1 },
weightedSupport: 1,
weightedSymbolFractions: { [symbol.toUpperCase()]: 1 },
};
return getResidueModeColor({
columnSummary: summary,
moleculeType: PROTEIN_RESIDUE_PALETTES.includes(palette)
? "protein"
: "dna",
palette,
symbol,
});
}
function parseOpaqueHexColor(
backgroundColor: string,
): [number, number, number] | null {
const match = /^#([\da-f]{6})$/i.exec(backgroundColor.trim());
if (match == null) {
return null;
}
const hex = match[1]!;
return [
Number.parseInt(hex.slice(0, 2), 16),
Number.parseInt(hex.slice(2, 4), 16),
Number.parseInt(hex.slice(4, 6), 16),
];
}
function getRelativeLuminance([red, green, blue]: [
number,
number,
number,
]): number {
const normalizedRed = getLinearizedColorChannel(red);
const normalizedGreen = getLinearizedColorChannel(green);
const normalizedBlue = getLinearizedColorChannel(blue);
return (
0.2126 * normalizedRed + 0.7152 * normalizedGreen + 0.0722 * normalizedBlue
);
}
function getLinearizedColorChannel(channel: number): number {
const normalized = channel / 255;
return normalized <= 0.04045
? normalized / 12.92
: ((normalized + 0.055) / 1.055) ** 2.4;
}
function getContrastRatio(first: number, second: number): number {
const lighter = Math.max(first, second);
const darker = Math.min(first, second);
return (lighter + 0.05) / (darker + 0.05);
}
function interpolateHex(from: string, to: string, rawFraction: number): string {
const fraction = Math.max(0, Math.min(1, rawFraction));
const fromRgb = parseOpaqueHexColor(from);
const toRgb = parseOpaqueHexColor(to);
if (fromRgb == null || toRgb == null) {
return from;
}
return `#${fromRgb
.map((channel, index) =>
Math.round(channel + ((toRgb[index] ?? channel) - channel) * fraction)
.toString(16)
.padStart(2, "0"),
)
.join("")}`;
}
const BLOSUM62_MATRIX = parseBlosumMatrix(`
A R N D C Q E G H I L K M F P S T W Y V B J Z X *
A 4 -1 -2 -2 0 -1 -1 0 -2 -1 -1 -1 -1 -2 -1 1 0 -3 -2 0 -2 -1 -1 -1 -4
R -1 5 0 -2 -3 1 0 -2 0 -3 -2 2 -1 -3 -2 -1 -1 -3 -2 -3 -1 -2 0 -1 -4
N -2 0 6 1 -3 0 0 0 1 -3 -3 0 -2 -3 -2 1 0 -4 -2 -3 4 -3 0 -1 -4
D -2 -2 1 6 -3 0 2 -1 -1 -3 -4 -1 -3 -3 -1 0 -1 -4 -3 -3 4 -3 1 -1 -4
C 0 -3 -3 -3 9 -3 -4 -3 -3 -1 -1 -3 -1 -2 -3 -1 -1 -2 -2 -1 -3 -1 -3 -1 -4
Q -1 1 0 0 -3 5 2 -2 0 -3 -2 1 0 -3 -1 0 -1 -2 -1 -2 0 -2 4 -1 -4
E -1 0 0 2 -4 2 5 -2 0 -3 -3 1 -2 -3 -1 0 -1 -3 -2 -2 1 -3 4 -1 -4
G 0 -2 0 -1 -3 -2 -2 6 -2 -4 -4 -2 -3 -3 -2 0 -2 -2 -3 -3 -1 -4 -2 -1 -4
H -2 0 1 -1 -3 0 0 -2 8 -3 -3 -1 -2 -1 -2 -1 -2 -2 2 -3 0 -3 0 -1 -4
I -1 -3 -3 -3 -1 -3 -3 -4 -3 4 2 -3 1 0 -3 -2 -1 -3 -1 3 -3 3 -3 -1 -4
L -1 -2 -3 -4 -1 -2 -3 -4 -3 2 4 -2 2 0 -3 -2 -1 -2 -1 1 -4 3 -3 -1 -4
K -1 2 0 -1 -3 1 1 -2 -1 -3 -2 5 -1 -3 -1 0 -1 -3 -2 -2 0 -3 1 -1 -4
M -1 -1 -2 -3 -1 0 -2 -3 -2 1 2 -1 5 0 -2 -1 -1 -1 -1 1 -3 2 -1 -1 -4
F -2 -3 -3 -3 -2 -3 -3 -3 -1 0 0 -3 0 6 -4 -2 -2 1 3 -1 -3 0 -3 -1 -4
P -1 -2 -2 -1 -3 -1 -1 -2 -2 -3 -3 -1 -2 -4 7 -1 -1 -4 -3 -2 -2 -3 -1 -1 -4
S 1 -1 1 0 -1 0 0 0 -1 -2 -2 0 -1 -2 -1 4 1 -3 -2 -2 0 -2 0 -1 -4
T 0 -1 0 -1 -1 -1 -1 -2 -2 -1 -1 -1 -1 -2 -1 1 5 -2 -2 0 -1 -1 -1 -1 -4
W -3 -3 -4 -4 -2 -2 -3 -2 -2 -3 -2 -3 -1 1 -4 -3 -2 11 2 -3 -4 -2 -2 -1 -4
Y -2 -2 -2 -3 -2 -1 -2 -3 2 -1 -1 -2 -1 3 -3 -2 -2 2 7 -1 -3 -1 -2 -1 -4
V 0 -3 -3 -3 -1 -2 -2 -3 -3 3 1 -2 1 -1 -2 -2 0 -3 -1 4 -3 2 -2 -1 -4
B -2 -1 4 4 -3 0 1 -1 0 -3 -4 0 -3 -3 -2 0 -1 -4 -3 -3 4 -3 0 -1 -4
J -1 -2 -3 -3 -1 -2 -3 -4 -3 3 3 -3 2 0 -3 -2 -1 -2 -1 2 -3 3 -3 -1 -4
Z -1 0 0 1 -3 4 4 -2 0 -3 -3 1 -1 -3 -1 0 -1 -2 -2 -2 0 -3 4 -1 -4
X -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -4
* -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 -4 1
`);
function parseBlosumMatrix(matrix: string): Map<string, number> {
const lines = matrix
.trim()
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0);
const header = lines[0]?.split(/\s+/) ?? [];
const scores = new Map<string, number>();
for (const line of lines.slice(1)) {
const [row, ...values] = line.split(/\s+/);
if (row == null) {
continue;
}
header.forEach((column, index) => {
const score = Number(values[index]);
if (Number.isFinite(score)) {
scores.set(`${row}:${column}`, score);
}
});
}
return scores;
}
function getProteinSimilarityScore({
columnSummary,
referenceSymbol,
symbol,
}: {
columnSummary: MsaColumnSummary;
referenceSymbol?: string;
symbol: string;
}): number | null {
if (isGapSymbol(symbol)) {
return null;
}
if (referenceSymbol != null) {
return getBlosum62Score(symbol, referenceSymbol);
}
let weightedScore = 0;
let support = 0;
for (const [candidate, fraction] of Object.entries(
columnSummary.weightedSymbolFractions,
)) {
const score = getBlosum62Score(symbol, candidate);
if (score == null) {
continue;
}
weightedScore += score * fraction;
support += fraction;
}
return support === 0 ? null : weightedScore / support;
}
function getBlosum62Score(first: string, second: string): number | null {
const normalizedFirst = first.toUpperCase();
const normalizedSecond = second.toUpperCase();
return (
BLOSUM62_MATRIX.get(`${normalizedFirst}:${normalizedSecond}`) ??
BLOSUM62_MATRIX.get(`${normalizedSecond}:${normalizedFirst}`) ??
null
);
}
export function getHydrophobicityGradient(): string {
return `linear-gradient(90deg, ${hydrophobicityColors.I}, ${
hydrophobicityColors.G
}, ${hydrophobicityColors.R})`;
}
export function getIdentityGradient(): string {
return `linear-gradient(90deg, ${identityLowColor}, ${identityHighColor})`;
}
export function getProteinConservationGradient(): string {
return `linear-gradient(90deg, ${proteinConservationLowColor}, ${proteinConservationHighColor})`;
}
export function getProteinSimilarityGradient(): string {
return `linear-gradient(90deg, ${proteinSimilarityLowColor} 0%, ${proteinSimilarityNeutralColor} 27%, ${proteinSimilarityHighColor} 100%)`;
}
SHA-256: 7728904ef592fc5955975a329e18f77040053233d97ff57cfa70e809818b472e