import { useId } from 'react'; import type { MappingSpec, TreeNode, TreeSpec, VennSpec } from './types'; import { Arrowheads, COLORS, INK, Label, SERIES, arrowId, mathText } from './svg'; const W = 320; /* ─── نمودارِ ون ────────────────────────────────────────────────────────── * ناحیه‌ها با نامِ مجموعه‌هایی که در آن‌اند: «A» فقط A، «AB» اشتراکِ A و B (و نه * C)، «ABC»، و «out» بیرونِ همه. سایه با برشِ تو در تو (clipPath) و ماسکِ * «نه-این-مجموعه» ساخته می‌شود، پس ناحیه‌ی سایه‌خورده دقیقاً همان ناحیه است. */ export function Venn({ spec }: { spec: VennSpec }) { const uid = useId().replace(/:/g, ''); const sets = spec.sets.slice(0, 3); if (sets.length < 1) return null; const H = sets.length === 3 ? 250 : 190; const R = sets.length === 3 ? 62 : 70; const centers: [number, number][] = sets.length === 1 ? [[W / 2, 95]] : sets.length === 2 ? [[W / 2 - 45, 95], [W / 2 + 45, 95]] : [[W / 2 - 40, 95], [W / 2 + 40, 95], [W / 2, 160]]; const ids = sets.map((_, i) => `v${uid}-${i}`); const keyOf = (inside: number[]) => inside.map((i) => sets[i]).join(''); // مرکزِ تقریبیِ هر ناحیه برای نوشتنِ اعضا const regionCenter = (code: string): [number, number] => { if (code === 'out') return [22, H - 16]; const inside = sets.map((s, i) => (code.includes(s) ? i : -1)).filter((i) => i >= 0); const all = centers.map((_, i) => i); const cxIn = inside.reduce((a, i) => a + centers[i][0], 0) / inside.length, cyIn = inside.reduce((a, i) => a + centers[i][1], 0) / inside.length; const outside = all.filter((i) => !inside.includes(i)); if (!outside.length || inside.length > 1) { if (inside.length === sets.length) return [cxIn, cyIn]; const o = outside.length ? centers[outside[0]] : [cxIn, cyIn]; return [cxIn + (cxIn - o[0]) * 0.18, cyIn + (cyIn - o[1]) * 0.18]; } const ox = outside.reduce((a, i) => a + centers[i][0], 0) / outside.length, oy = outside.reduce((a, i) => a + centers[i][1], 0) / outside.length; const dx = cxIn - ox, dy = cyIn - oy, n = Math.hypot(dx, dy) || 1; return [cxIn + (dx / n) * R * 0.42, cyIn + (dy / n) * R * 0.42]; }; const shaded = (spec.shade ?? []).filter((c) => c === 'out' || [...c].every((ch) => sets.includes(ch))); const shadeRegion = (code: string, idx: number) => { const fill = `${COLORS.orange}66`; if (code === 'out') { return ( {centers.map((c, i) => )} ); } const inside = sets.map((s, i) => (code.includes(s) ? i : -1)).filter((i) => i >= 0); const outside = sets.map((_, i) => i).filter((i) => !inside.includes(i)); const mid = `${ids[0]}-m${idx}`; let node = ( {outside.length > 0 && {outside.map((i) => )}} ); for (const i of inside) node = {node}; return {node}; }; return ( {centers.map((c, i) => )} U {shaded.map(shadeRegion)} {centers.map((c, i) => { const col = COLORS[SERIES[i]]; return ( ); })} {Object.entries(spec.regions).map(([code, members]) => { const valid = code === 'out' || [...code].every((ch) => sets.includes(ch)); if (!valid || !members.length) return null; const [x, y] = regionCenter(code === 'out' ? 'out' : keyOf(sets.map((s, i) => (code.includes(s) ? i : -1)).filter((i) => i >= 0))); const lines = chunk(members.slice(0, 12), 3); return lines.map((line, li) => ( )); })} ); } const chunk = (xs: T[], n: number): T[][] => Array.from({ length: Math.ceil(xs.length / n) }, (_, i) => xs.slice(i * n, i * n + n)); /* ─── نمودارِ پیکانی (تابع از A به B) ─────────────────────────────────────── */ export function Mapping({ spec }: { spec: MappingSpec }) { const L = spec.left.slice(0, 10), Rt = spec.right.slice(0, 10); const rows = Math.max(L.length, Rt.length, 1); const H = 56 + rows * 28; const yOf = (i: number, n: number) => 40 + ((i + 0.5) * (rows * 28)) / n; // جهتِ نگاشت چپ به راست است، مثلِ کتاب (دامنه سمتِ چپ) const lx = 78, rx = W - 78; const col = COLORS.red; return ( {[[lx, L, spec.leftLabel, COLORS.blue], [rx, Rt, spec.rightLabel, COLORS.green]].map(([x, items, label, c]) => ( {label && } {(items as string[]).map((it, i) => ( ))} ))} {spec.arrows.filter(([a, b]) => a < L.length && b < Rt.length).map(([a, b], i) => ( ))} ); } /* ─── نمودارِ درختی (احتمال و شمارش) ──────────────────────────────────────── */ export function Tree({ spec }: { spec: TreeSpec }) { interface Laid { node: TreeNode; x: number; y: number; depth: number; parent?: Laid } const laid: Laid[] = []; let leaf = 0; const depthOf = (n: TreeNode): number => 1 + Math.max(0, ...(n.children ?? []).map(depthOf)); const depth = Math.min(4, depthOf(spec.root)); const levelH = 62; const place = (n: TreeNode, d: number, parent?: Laid): Laid => { const kids = d < 3 ? (n.children ?? []).slice(0, 6) : []; const me: Laid = { node: n, x: 0, y: 24 + d * levelH, depth: d, parent }; if (!kids.length) { me.x = leaf++; laid.push(me); return me; } const placed = kids.map((c) => place(c, d + 1, me)); me.x = (placed[0].x + placed[placed.length - 1].x) / 2; laid.push(me); return me; }; place(spec.root, 0); const leaves = Math.max(1, leaf); const H = 24 + (depth - 1) * levelH + 34; const X = (x: number) => 24 + ((x + 0.5) / leaves) * (W - 48); return ( {laid.filter((n) => n.parent).map((n, i) => { const p = n.parent!; return ( {n.node.p && } ); })} {laid.map((n, i) => ( {mathText(n.node.label).slice(0, 10)} ))} ); }