"use client"; import { motion } from "framer-motion"; import { useState } from "react"; import { COLORS, VizFrame } from "./common"; function rng(seed: number) { let s = seed; return () => { s = (s * 1664525 + 1013904223) % 4294967296; return s / 4294967296; }; } function generate() { const r = rng(11); const pts: { x: number; y: number; c: 0 | 1 | 2 }[] = []; const centers: [number, number, 0 | 1 | 2][] = [ [0.25, 0.3, 0], [0.75, 0.35, 1], [0.5, 0.78, 2], ]; for (const [cx, cy, cls] of centers) { for (let i = 0; i < 16; i++) { pts.push({ x: cx + (r() - 0.5) * 0.32, y: cy + (r() - 0.5) * 0.32, c: cls, }); } } return pts; } const POINTS = generate(); const PALETTE = [COLORS.accent, COLORS.honey, COLORS.green]; export function KnnQuery({ width = 720, height = 440, }: { width?: number; height?: number; }) { const [k, setK] = useState(5); const [query, setQuery] = useState({ x: 0.5, y: 0.5 }); const padX = 40; const padY = 30; const sx = (x: number) => padX + x * (width - padX * 2); const sy = (y: number) => height - padY - y * (height - padY * 2); // Distances const sorted = POINTS .map((p) => ({ ...p, d: Math.hypot(p.x - query.x, p.y - query.y) })) .sort((a, b) => a.d - b.d); const neighbours = sorted.slice(0, k); const radius = neighbours[neighbours.length - 1]?.d ?? 0; // Vote const votes = [0, 0, 0]; neighbours.forEach((n) => votes[n.c]++); const winner = votes.indexOf(Math.max(...votes)) as 0 | 1 | 2; // Convert radius to screen coordinates correctly: radius is in unit space, // multiply by the same scale factor as sx/sy axes (they are equal). const radiusPx = radius * (width - padX * 2); function move(e: React.MouseEvent) { const rect = e.currentTarget.getBoundingClientRect(); const px = e.clientX - rect.left; const py = e.clientY - rect.top; const sxRatio = width / rect.width; const syRatio = height / rect.height; const xPx = px * sxRatio; const yPx = py * syRatio; const x = (xPx - padX) / (width - padX * 2); const y = 1 - (yPx - padY) / (height - padY * 2); setQuery({ x: Math.max(0.05, Math.min(0.95, x)), y: Math.max(0.05, Math.min(0.95, y)), }); } return (
{/* k circle */} {POINTS.map((p, i) => { const isN = neighbours.find((n) => n.x === p.x && n.y === p.y); return ( ); })} {/* Lines to neighbours */} {neighbours.map((n, i) => ( ))} {/* Query */}
vote → {votes.join(" / ")} · winner = class {winner}
); }