import { useState } from 'react' function colorFor(v) { if (v == null) return '#0F1729' if (v < 30) { const t = v / 30 return `rgb(${Math.round(15 + t * 10)}, ${Math.round(28 + t * 40)}, ${Math.round(60 + t * 80)})` } if (v < 60) { const t = (v - 30) / 30 return `rgb(${Math.round(25 + t * (0 - 25))}, ${Math.round(68 + t * (212 - 68))}, ${Math.round(140 + t * (255 - 140))})` } if (v < 80) { const t = (v - 60) / 20 return `rgb(${Math.round(t * 245)}, ${Math.round(212 + t * (158 - 212))}, ${Math.round(255 + t * (11 - 255))})` } const t = (v - 80) / 20 return `rgb(${Math.round(245 + t * (239 - 245))}, ${Math.round(158 + t * (68 - 158))}, ${Math.round(11 + t * (68 - 11))})` } export default function Heatmap({ data, ids, onSelectPair }) { const [tip, setTip] = useState(null) const [selected, setSelected] = useState(null) // { i, j } if (!data || !ids || ids.length === 0) { return
No data.
} const n = ids.length const cellSize = Math.max(20, Math.min(80, Math.floor(480 / n))) const labelW = 72 const showPct = cellSize >= 36 const handleClick = (i, j) => { if (i === j) return const newSel = selected?.i === i && selected?.j === j ? null : { i, j } setSelected(newSel) if (onSelectPair) { onSelectPair(newSel ? { i, j, idA: ids[i], idB: ids[j], similarity: Math.round(data[i][j] * 100) } : null) } } return (
{n < 3 && (
Heatmap je najkorisniji s 3 ili više datoteka.
)}
{/* Gornji labeli */}
{ids.map((id, i) => (
{id}
))}
{/* Grid */}
{/* Lijevi labeli */}
{ids.map((id, i) => (
{id}
))}
{/* Ćelije */}
{data.map((row, i) => (
{row.map((v, j) => { const pct = Math.round(v * 100) const isDiag = i === j const isSel = selected?.i === i && selected?.j === j const bg = isDiag ? '#1F2937' : colorFor(pct) const textColor = pct > 45 && pct < 75 ? '#0A0F1C' : '#FFFFFF' return (
setTip({ x: e.clientX, y: e.clientY, a: ids[i], b: ids[j], pct, isDiag })} onMouseMove={e => setTip({ x: e.clientX, y: e.clientY, a: ids[i], b: ids[j], pct, isDiag })} onMouseLeave={() => setTip(null)} onClick={() => handleClick(i, j)} style={{ width: cellSize, height: cellSize, flexShrink: 0, background: bg, border: isSel ? '2px solid #00D4FF' : '1px solid rgba(10,15,28,0.3)', cursor: isDiag ? 'default' : 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', boxSizing: 'border-box', outline: isSel ? '2px solid rgba(0,212,255,0.4)' : 'none', transition: 'filter .1s', }}> {showPct && !isDiag && ( {pct}% )} {isDiag && showPct && ( )}
) })}
))}
{/* Legenda */}
Similarity
{Array.from({ length: 40 }).map((_, i) => (
))}
0% 50% 75% 100%
{selected && ( Klikni ponovo za deselect · skrolaj dolje za usporedbu )}
{/* Tooltip */} {tip && !tip.isDiag && (
{tip.a} ↔ {tip.b}
{tip.pct}%
{tip.pct >= 80 ? 'Very high similarity' : tip.pct >= 60 ? 'High similarity' : tip.pct >= 40 ? 'Moderate' : 'Low similarity'}
Klikni za usporedbu koda
)}
) }