// ===================================================================== // 行当 Sunburst — Task 1: role-type classification // Inner ring: 生 旦 净 丑 ; Outer ring: 老生/小生/... // Selection brushes across to network + theme heatmap // ===================================================================== function RoleSunburst({ plays, focusPlay, selectedRole, onSelectRole, hoverChar, characterIndex }) { const [wrap, size] = useSize(); // collect chars matching filter const chars = useMemo(() => { const list = []; plays.forEach(p => { if (focusPlay && p.id !== focusPlay) return; p.characters.forEach(c => list.push({ ...c, playId: p.id, playTitle: p.title, genre: p.genre })); }); return list; }, [plays, focusPlay]); // aggregate const tree = useMemo(() => { const t = {}; chars.forEach(c => { if (!t[c.role]) t[c.role] = { total: 0, subs: {} }; t[c.role].total++; if (!t[c.role].subs[c.sub]) t[c.role].subs[c.sub] = { total: 0, chars: [] }; t[c.role].subs[c.sub].total++; t[c.role].subs[c.sub].chars.push(c); }); return t; }, [chars]); const roleOrder = ["sheng","dan","jing","chou"]; const totalChars = chars.length || 1; const W = size.w || 360; const H = size.h || 280; const cx = W / 2, cy = H / 2 + 4; const rInner = 28; const rMid = Math.min(W, H) * 0.30; const rOuter = Math.min(W, H) * 0.46; // build arcs let acc = 0; const innerArcs = []; const outerArcs = []; roleOrder.forEach(role => { if (!tree[role]) return; const span = tree[role].total / totalChars; const a0 = acc * Math.PI * 2 - Math.PI / 2; const a1 = (acc + span) * Math.PI * 2 - Math.PI / 2; innerArcs.push({ role, a0, a1, count: tree[role].total }); let subAcc = acc; Object.entries(tree[role].subs).forEach(([sub, info]) => { const subSpan = info.total / totalChars; const sa0 = subAcc * Math.PI * 2 - Math.PI / 2; const sa1 = (subAcc + subSpan) * Math.PI * 2 - Math.PI / 2; outerArcs.push({ role, sub, a0: sa0, a1: sa1, count: info.total, chars: info.chars }); subAcc += subSpan; }); acc += span; }); function arcPath(a0, a1, rIn, rOut) { const sx0 = cx + rOut * Math.cos(a0), sy0 = cy + rOut * Math.sin(a0); const sx1 = cx + rOut * Math.cos(a1), sy1 = cy + rOut * Math.sin(a1); const ex0 = cx + rIn * Math.cos(a1), ey0 = cy + rIn * Math.sin(a1); const ex1 = cx + rIn * Math.cos(a0), ey1 = cy + rIn * Math.sin(a0); const large = (a1 - a0) > Math.PI ? 1 : 0; return `M ${sx0} ${sy0} A ${rOut} ${rOut} 0 ${large} 1 ${sx1} ${sy1} L ${ex0} ${ey0} A ${rIn} ${rIn} 0 ${large} 0 ${ex1} ${ey1} Z`; } return (