File size: 3,203 Bytes
bac1801
342fa92
 
773cf91
bac1801
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342fa92
bac1801
 
 
 
 
 
 
 
 
deb8696
 
bac1801
 
 
 
 
 
 
 
 
 
 
342fa92
 
bac1801
 
 
 
 
 
 
342fa92
bac1801
 
 
 
 
 
 
 
 
 
 
 
 
 
deb8696
 
bac1801
 
 
 
 
 
 
 
 
 
 
 
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
"use client";
import { useRef } from "react";
import { exportSvgPng } from "@/lib/export-utils";
import { groupOf } from "@/lib/alignment-stats";

function columnConservation(column: string[]): number {
  const filtered = column.filter(c => c !== "-" && c !== ".");
  if (!filtered.length) return 0;

  const freqs = new Map<string, number>();
  for (const aa of filtered) freqs.set(aa, (freqs.get(aa) ?? 0) + 1);
  const maxFreq = Math.max(...Array.from(freqs.values()));
  const identityScore = maxFreq / filtered.length;

  const groups = new Set(filtered.map(groupOf));
  const groupScore = Array.from(groups).length === 1 ? 0.15 : 0;

  return Math.min(1, identityScore + groupScore);
}

interface Props {
  alignedSeqs: string[];
}

export function ConservationTrack({ alignedSeqs }: Props) {
  const svgRef = useRef<SVGSVGElement>(null);
  if (!alignedSeqs.length) return null;
  const L = alignedSeqs[0].length;

  const scores: number[] = [];
  for (let i = 0; i < L; i++) {
    scores.push(columnConservation(alignedSeqs.map(s => s[i] ?? "-")));
  }

  const scoreColor = (s: number) => {
    if (s > 0.9) return "#4ADE80";
    if (s > 0.7) return "#FBBF24";
    if (s > 0.4) return "#7C3AED";
    return "rgba(255,255,255,0.1)";
  };

  const avgConservation = (scores.reduce((a, b) => a + b, 0) / scores.length * 100).toFixed(1);
  const highlyConserved = scores.filter(s => s > 0.9).length;

  return (
    <div className="space-y-3">
      <div className="flex items-center justify-between">
        <h3 className="text-text-primary font-semibold">Conservation Analysis</h3>
        <button onClick={() => exportSvgPng(svgRef.current, "conservation.png")}
          className="btn-ghost text-xs px-2 py-1">Export PNG</button>
        <div className="flex gap-4 text-xs text-text-muted">
          <span>Avg: <span className="text-text-secondary">{avgConservation}%</span></span>
          <span>Fully conserved: <span className="text-accent-cyan">{highlyConserved} positions</span></span>
        </div>
      </div>

      <div className="bg-surface-1 rounded-xl p-3 border border-glass-border overflow-x-auto">
        <svg ref={svgRef} viewBox={`0 0 ${L * 4} 60`} className="w-full" style={{ minWidth: L * 4 }}>
          {scores.map((s, i) => (
            <rect key={i} x={i * 4} y={60 - s * 55} width={3} height={s * 55}
              fill={scoreColor(s)} rx={1} />
          ))}
        </svg>
        <div className="flex justify-between text-xs text-text-muted mt-1 font-mono">
          <span>1</span>
          <span>{Math.floor(L / 2)}</span>
          <span>{L}</span>
        </div>
      </div>

      <div className="flex gap-4 text-xs text-text-muted">
        {[
          { color: "#4ADE80", label: ">90% conserved" },
          { color: "#FBBF24", label: ">70% conserved" },
          { color: "#7C3AED", label: ">40% conserved" },
          { color: "rgba(255,255,255,0.2)", label: "Variable" },
        ].map(({ color, label }) => (
          <span key={label} className="flex items-center gap-1.5">
            <span className="w-2.5 h-2.5 rounded-sm" style={{ background: color }} />
            {label}
          </span>
        ))}
      </div>
    </div>
  );
}