// Reusable small components: pills, sigstrip, radar glyph, sequence strip, helpers. const isSig = (q) => q != null && q < 0.05; function fmt(v, d = 2) { if (v == null) return "—"; return Number(v).toFixed(d); } function fmtQ(q) { if (q == null) return "—"; if (q < 1e-3) return q.toExponential(1).replace("e", "·10"); return q.toFixed(3); } function Pill({ kind, children }) { return {children}; } // Per-axis colours for the vertex dots. Order matches the radar axes: // curvature, torsion, planarity, compactness, contacts, composition. // Same hues the old viz used so the visual identity is preserved. const RADAR_AXIS_COLORS = [ "#3b82f6", // curvature — blue "#ef4444", // torsion — red "#22c55e", // planarity — green "#f97316", // compactness — orange "#8b5cf6", // contacts — purple "#14b8a6", // composition — teal ]; // Hexagonal radar glyph. Renders 2 concentric guide rings, 6 axis spokes, // the data polygon (filled + stroked in the geom accent), and a colored // dot at each vertex so the user can read which axis is which at a glance. // `scores` is a length-6 array in [0..1] for axes: // curvature, torsion, planarity, compactness, contacts, composition. function RadarGlyph({ scores, size = 28, color = "var(--geom)" }) { if (!scores || !Array.isArray(scores) || scores.length < 6) { return ; } const cx = size / 2, cy = size / 2, r = size / 2 - 2; const n = 6; // Vertex dots/decorations get suppressed at very small sizes so the // 28px table glyph stays clean. const detailed = size >= 60; const angle = (i) => (Math.PI * 2 * i) / n - Math.PI / 2; const vertexAt = (i, radius) => [cx + Math.cos(angle(i)) * radius, cy + Math.sin(angle(i)) * radius]; const dataPts = scores.slice(0, 6).map((s, i) => { const v = Math.max(0.02, Math.min(1, s ?? 0)); return vertexAt(i, r * v); }); const polyPts = dataPts.map((p) => p.join(",")).join(" "); const outerRingPts = Array.from({ length: n }, (_, i) => vertexAt(i, r).join(",")).join(" "); const innerRingPts = Array.from({ length: n }, (_, i) => vertexAt(i, r * 0.5).join(",")).join(" "); return ( ); } // 7-dot strip showing per-method significance status. function SigStrip({ feat }) { const dots = []; for (let k = 1; k <= 7; k++) { const sig = isSig(feat[`m${k}_q`]); const cls = sig ? (k === 7 ? "sd geom" : "sd bio") : "sd"; dots.push(); } return {dots}; } function bioBest(feat) { const xs = [feat.m1_score, feat.m2_score, feat.m3_score, feat.m4_score, feat.m5_score, feat.m6_score] .filter((v) => v != null); return xs.length ? Math.max(...xs) : null; } function bioSig(feat) { for (let k = 1; k <= 6; k++) if (isSig(feat[`m${k}_q`])) return true; return false; } function geomSig(feat) { return isSig(feat.m7_q); } function rowCategory(feat) { const g = geomSig(feat), b = bioSig(feat); if (g && b) return "both"; if (g) return "geom_only"; if (b) return "bio_only"; return "none"; } // Convert a geometry_radar dict {curvature, torsion, ...} to ordered array. function radarFromObj(obj) { if (!obj) return null; return [ obj.curvature, obj.torsion, obj.planarity, obj.compactness, obj.contacts, obj.composition, ].map(v => Number(v) || 0); } // Boot/loading and error states function Loading({ what }) { return