Spaces:
Running
Running
File size: 4,937 Bytes
ec313db deb8696 ec313db deb8696 ec313db e83084c ec313db deb8696 ec313db deb8696 ec313db | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | 'use client';
import { useMemo } from 'react';
import { analyzeColumns, parseAlignedFasta } from '@/lib/alignment-stats';
const AA_COLORS: Record<string, string> = {
A: '#A78BFA', C: '#FBBF24', D: '#EF4444', E: '#EF4444',
F: '#7C3AED', G: '#848CA4', H: '#60A5FA', I: '#7C3AED',
K: '#4ADE80', L: '#7C3AED', M: '#FBBF24', N: '#60A5FA',
P: '#FB923C', Q: '#60A5FA', R: '#4ADE80', S: '#60A5FA',
T: '#60A5FA', V: '#7C3AED', W: '#7C3AED', Y: '#7C3AED',
};
function aaColor(ch: string): string {
if (ch === '-') return 'rgba(132,140,164,0.25)';
return AA_COLORS[ch.toUpperCase()] ?? 'rgb(var(--text-primary))';
}
type CellClass = 'match' | 'mismatch' | 'gap';
function cellClass(ch: string, consensus: string): CellClass {
if (ch === '-' || ch === '.') return 'gap';
return ch.toUpperCase() === consensus ? 'match' : 'mismatch';
}
const CELL_BG: Record<CellClass, string> = {
match: 'rgba(74,222,128,0.16)',
mismatch: 'rgba(239,68,68,0.12)',
gap: 'rgba(132,140,164,0.08)',
};
function symbolClass(symbol: string): string {
if (symbol === '*') return 'text-accent-cyan font-bold';
if (symbol === ':') return 'text-accent-amber';
if (symbol === '.') return 'text-text-muted/40';
return 'text-transparent';
}
interface AlignmentBlockProps {
/** Aligned FASTA text (headers + equal-length gapped sequences). */
alnFasta: string;
/** Extra classes for the scroll container (e.g. max-height). */
className?: string;
}
/**
* Colored MSA grid shared by every alignment surface. Each cell is tinted by
* its relationship to the column consensus — green = matched, red = mismatched,
* gray = gap — with the per-residue chemistry color kept as the foreground and
* a CLUSTAL-style consensus row (* : .) beneath the sequences.
*/
export function AlignmentBlock({ alnFasta, className = '' }: AlignmentBlockProps) {
const { headers, seqs } = useMemo(() => parseAlignedFasta(alnFasta), [alnFasta]);
const length = seqs[0]?.length ?? 0;
const columns = useMemo(() => analyzeColumns(seqs), [seqs]);
if (!seqs.length) return null;
return (
<div className={`rounded-xl border border-glass-border bg-surface-1 overflow-auto ${className}`}>
<div className="min-w-max p-3 font-mono text-xs leading-[1.7]">
<div className="flex">
<span className="w-24 shrink-0 pr-3 text-right text-[10px] text-text-muted/60" />
<span className="whitespace-pre text-[10px] text-text-muted/50">
{Array.from({ length }).map((_, i) => ((i + 1) % 10 === 0 ? (i + 1) / 10 % 10 : ' ').toString()).join('')}
</span>
</div>
{headers.map((h, i) => (
<div key={i} className="flex">
<span
className="w-24 shrink-0 pr-3 text-right text-[10px] text-accent-cyan/80 truncate"
title={h}
>
{h.length > 12 ? `${h.slice(0, 11)}…` : h}
</span>
<span className="whitespace-pre" style={{ color: 'inherit' }}>
{Array.from(seqs[i] ?? '').map((ch, j) => {
const cls = cellClass(ch, columns[j]?.consensus ?? '-');
return (
<span key={j} style={{ color: aaColor(ch), backgroundColor: CELL_BG[cls] }}>
{ch}
</span>
);
})}
</span>
</div>
))}
<div className="flex">
<span className="w-24 shrink-0 pr-3 text-right text-[10px] text-accent-amber/80 truncate">
consensus
</span>
<span className="whitespace-pre">
{columns.map((col, j) => (
<span key={j} className={symbolClass(col.symbol)}>
{col.symbol}
</span>
))}
</span>
</div>
</div>
<div className="px-3 pb-3 flex flex-wrap items-center gap-x-5 gap-y-2 text-[10px] text-text-muted">
<span className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-sm" style={{ background: CELL_BG.match }} />
Matched
</span>
<span className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-sm" style={{ background: CELL_BG.mismatch }} />
Mismatched
</span>
<span className="flex items-center gap-1.5">
<span className="w-2.5 h-2.5 rounded-sm" style={{ background: CELL_BG.gap }} />
Gap
</span>
<span className="flex items-center gap-1.5">
<span className="font-bold text-accent-cyan">*</span> Identical
</span>
<span className="flex items-center gap-1.5">
<span className="text-accent-amber font-bold">:</span> Strongly conserved group
</span>
<span className="flex items-center gap-1.5">
<span className="text-text-muted/50 font-bold">.</span> Weakly conserved
</span>
</div>
</div>
);
}
|