| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Kuru-Graph: Mahabharata Network</title> |
| |
| |
| <script src="https://cdn.tailwindcss.com"></script> |
| |
| |
| <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script> |
| |
| |
| <script src="https://unpkg.com/lucide@latest"></script> |
|
|
| <style> |
| body { margin: 0; padding: 0; overflow: hidden; background-color: #111827; color: white; } |
| #network-container { width: 100vw; height: 100vh; } |
| |
| |
| .vis-network:focus { outline: none; } |
| |
| |
| ::-webkit-scrollbar { width: 6px; } |
| ::-webkit-scrollbar-track { background: #1f2937; } |
| ::-webkit-scrollbar-thumb { background: #4b5563; border-radius: 4px; } |
| ::-webkit-scrollbar-thumb:hover { background: #6b7280; } |
| |
| .glass-panel { |
| background: rgba(31, 41, 55, 0.85); |
| backdrop-filter: blur(12px); |
| -webkit-backdrop-filter: blur(12px); |
| border: 1px solid rgba(75, 85, 99, 0.4); |
| } |
| </style> |
| </head> |
| <body class="font-sans antialiased text-gray-100 selection:bg-indigo-500 selection:text-white"> |
|
|
| |
| <div id="network-container" class="absolute inset-0 z-0"></div> |
|
|
| |
| <div class="absolute top-0 left-0 w-full p-4 md:p-6 z-10 pointer-events-none flex justify-between items-start"> |
| <div class="pointer-events-auto"> |
| <h1 class="text-3xl md:text-4xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-amber-400 to-orange-500 flex items-center gap-3 shadow-sm"> |
| <i data-lucide="network"></i> Kuru-Graph |
| </h1> |
| <p class="text-gray-400 text-sm mt-1 max-w-xs">An interactive character network of the Mahabharata. Scroll to zoom, drag to pan.</p> |
| </div> |
|
|
| |
| <div class="pointer-events-auto relative mt-2 md:mt-0"> |
| <div class="relative"> |
| <i data-lucide="search" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4"></i> |
| <input type="text" id="searchInput" placeholder="Search character..." |
| class="bg-gray-800/80 border border-gray-700 text-white rounded-full pl-10 pr-4 py-2 w-48 md:w-64 focus:outline-none focus:border-amber-500 focus:ring-1 focus:ring-amber-500 transition-all shadow-lg backdrop-blur-md"> |
| </div> |
| <ul id="searchResults" class="absolute top-full left-0 w-full mt-2 bg-gray-800 border border-gray-700 rounded-lg shadow-xl max-h-60 overflow-y-auto hidden"> |
| |
| </ul> |
| </div> |
| </div> |
|
|
| |
| <div class="absolute bottom-6 left-6 z-10 pointer-events-auto glass-panel p-4 rounded-xl shadow-2xl hidden md:block"> |
| <h3 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-3">Factions</h3> |
| <div class="grid grid-cols-2 gap-x-6 gap-y-2 text-sm" id="legendContainer"> |
| |
| </div> |
| <div class="mt-4 pt-3 border-t border-gray-700"> |
| <h3 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Relationships</h3> |
| <div class="flex items-center gap-2 text-xs text-gray-300"><span class="w-4 h-0.5 bg-gray-400"></span> Family / Neutral</div> |
| <div class="flex items-center gap-2 text-xs text-gray-300"><span class="w-4 h-0.5 bg-pink-500"></span> Spouse</div> |
| <div class="flex items-center gap-2 text-xs text-gray-300"><span class="w-4 h-0.5 border-t border-dashed border-red-500"></span> Rivalry</div> |
| <div class="flex items-center gap-2 text-xs text-gray-300"><span class="w-4 h-0.5 bg-purple-500"></span> Mentor / Student</div> |
| </div> |
| </div> |
|
|
| |
| <div id="sidebar" class="fixed right-0 top-0 h-full w-full md:w-96 glass-panel transform translate-x-full transition-transform duration-300 ease-in-out z-20 shadow-2xl flex flex-col pointer-events-auto"> |
| |
| |
| <div class="p-6 pb-4 border-b border-gray-700 flex justify-between items-start relative"> |
| <div> |
| <span id="charFaction" class="px-2 py-1 text-xs font-semibold rounded-md bg-gray-700 text-gray-300 mb-2 inline-block">Faction</span> |
| <h2 id="charName" class="text-3xl font-bold text-white mb-1">Character Name</h2> |
| <p id="charTitle" class="text-amber-400 text-sm italic">Title / Role</p> |
| </div> |
| <button id="closeSidebar" class="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-full transition-colors"> |
| <i data-lucide="x" class="w-5 h-5"></i> |
| </button> |
| </div> |
|
|
| |
| <div class="p-6 flex-1 overflow-y-auto"> |
| <h3 class="text-lg font-semibold border-b border-gray-700 pb-2 mb-3">About</h3> |
| <p id="charDesc" class="text-gray-300 text-sm leading-relaxed mb-6"> |
| Character description goes here. |
| </p> |
|
|
| <h3 class="text-lg font-semibold border-b border-gray-700 pb-2 mb-3">Key Relationships</h3> |
| <ul id="charRelations" class="space-y-3"> |
| |
| </ul> |
| </div> |
| |
| |
| <div class="p-4 border-t border-gray-700 bg-gray-800/50"> |
| <button id="focusNodeBtn" class="w-full py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg transition-colors flex items-center justify-center gap-2 font-medium"> |
| <i data-lucide="target" class="w-4 h-4"></i> Center in Graph |
| </button> |
| </div> |
| </div> |
|
|
| |
| <button id="mobileLegendBtn" class="md:hidden absolute bottom-6 left-6 z-10 glass-panel p-3 rounded-full text-white pointer-events-auto shadow-lg"> |
| <i data-lucide="info" class="w-6 h-6"></i> |
| </button> |
|
|
| <script> |
| |
| lucide.createIcons(); |
| |
| |
| |
| |
| const groups = { |
| Pandava: { color: { background: '#2563eb', border: '#1d4ed8', hover: { background: '#3b82f6', border: '#2563eb' } } }, |
| Kaurava: { color: { background: '#dc2626', border: '#b91c1c', hover: { background: '#ef4444', border: '#dc2626' } } }, |
| Elder: { color: { background: '#d97706', border: '#b45309', hover: { background: '#f59e0b', border: '#d97706' } } }, |
| Yadava: { color: { background: '#059669', border: '#047857', hover: { background: '#10b981', border: '#059669' } } }, |
| Teacher: { color: { background: '#7c3aed', border: '#6d28d9', hover: { background: '#8b5cf6', border: '#7c3aed' } } }, |
| Panchala: { color: { background: '#db2777', border: '#be185d', hover: { background: '#ec4899', border: '#db2777' } } }, |
| Divine: { color: { background: '#0891b2', border: '#0e7490', hover: { background: '#06b6d4', border: '#0891b2' } } }, |
| Neutral: { color: { background: '#4b5563', border: '#374151', hover: { background: '#6b7280', border: '#4b5563' } } } |
| }; |
| |
| |
| const edgeStyles = { |
| Parent: { color: '#9ca3af', dashes: false }, |
| Spouse: { color: '#ec4899', dashes: false }, |
| Rival: { color: '#ef4444', dashes: [5, 5] }, |
| Teacher: { color: '#a855f7', dashes: false }, |
| Friend: { color: '#10b981', dashes: false }, |
| Sibling: { color: '#60a5fa', dashes: false }, |
| Default: { color: '#6b7280', dashes: false } |
| }; |
| |
| |
| const rawNodes = [ |
| { id: 1, label: "Shantanu", group: "Elder", title: "King of Hastinapur", desc: "A great king of the Kuru dynasty, ancestor to both Pandavas and Kauravas." }, |
| { id: 2, label: "Ganga", group: "Divine", title: "River Goddess", desc: "First wife of Shantanu, mother of Bhishma." }, |
| { id: 3, label: "Bhishma", group: "Elder", title: "The Grandsire", desc: "Born Devavrata. Took a vow of lifelong celibacy and service to the throne of Hastinapur. A peerless warrior." }, |
| { id: 4, label: "Satyavati", group: "Elder", title: "Queen Mother", desc: "Second wife of Shantanu. Mother of Vyasa, Chitrangada, and Vichitravirya." }, |
| { id: 5, label: "Vyasa", group: "Divine", title: "The Sage/Author", desc: "Biological father of Dhritarashtra, Pandu, and Vidura via Niyoga. The traditional author of the epic." }, |
| { id: 6, label: "Dhritarashtra", group: "Kaurava", title: "Blind King", desc: "The blind king of Hastinapur, father of the Kauravas. Torn between dharma and his love for his son Duryodhana." }, |
| { id: 7, label: "Pandu", group: "Pandava", title: "Former King", desc: "Younger brother of Dhritarashtra. Cursed to die if he engaged in marital relations. Father of the Pandavas via divine boons." }, |
| { id: 8, label: "Vidura", group: "Elder", title: "Prime Minister", desc: "Incarnation of Yama (Dharma). Known for his unparalleled wisdom and adherence to righteousness." }, |
| { id: 9, label: "Gandhari", group: "Kaurava", title: "Queen of Hastinapur", desc: "Wife of Dhritarashtra. Blindfolded herself out of solidarity with her blind husband." }, |
| { id: 10, label: "Kunti", group: "Pandava", title: "Mother of Pandavas", desc: "First wife of Pandu. Possessed a mantra to invoke gods to grant her children." }, |
| { id: 11, label: "Madri", group: "Pandava", title: "Second Queen", desc: "Second wife of Pandu. Mother of the twins Nakula and Sahadeva. Committed Sati on Pandu's pyre." }, |
| { id: 12, label: "Yudhishthira", group: "Pandava", title: "Eldest Pandava", desc: "Son of Yama (Dharma). Known for his absolute, sometimes stubborn, adherence to truth and righteousness." }, |
| { id: 13, label: "Bhima", group: "Pandava", title: "The Mighty", desc: "Son of Vayu (Wind). Possessed the strength of ten thousand elephants. Vowed to kill all Kauravas." }, |
| { id: 14, label: "Arjuna", group: "Pandava", title: "The Great Archer", desc: "Son of Indra. The central hero of the epic, a peerless archer, and recipient of the Bhagavad Gita." }, |
| { id: 15, label: "Nakula", group: "Pandava", title: "The Handsome", desc: "Son of the Ashvins. Unmatched in beauty and a master of sword fighting and horsemanship." }, |
| { id: 16, label: "Sahadeva", group: "Pandava", title: "The Scholar", desc: "Son of the Ashvins. A master of astrology and swordsmanship. Swore to kill Shakuni." }, |
| { id: 17, label: "Duryodhana", group: "Kaurava", title: "Eldest Kaurava", desc: "The primary antagonist. Driven by envy and hatred toward his cousins, the Pandavas." }, |
| { id: 18, label: "Dushasana", group: "Kaurava", title: "Second Kaurava", desc: "Duryodhana's loyal brother. Infamous for dragging Draupadi by her hair." }, |
| { id: 19, label: "Karna", group: "Kaurava", title: "King of Anga", desc: "Eldest son of Kunti (abandoned). Raised by a charioteer. A tragic hero of immense generosity and martial skill, allied with Duryodhana." }, |
| { id: 20, label: "Draupadi", group: "Panchala", title: "Empress of Indraprastha", desc: "Born from fire. Common wife to the five Pandavas. Her humiliation fueled the great war." }, |
| { id: 21, label: "Krishna", group: "Yadava", title: "Avatar of Vishnu", desc: "Cousin and primary ally of the Pandavas. The supreme strategist and speaker of the Bhagavad Gita." }, |
| { id: 22, label: "Balarama", group: "Yadava", title: "Elder Brother of Krishna", desc: "Teacher of the mace to both Bhima and Duryodhana. Remained neutral during the war." }, |
| { id: 23, label: "Drona", group: "Teacher", title: "Royal Preceptor", desc: "Master of advanced military arts. Taught both Kauravas and Pandavas, but fought for Hastinapur due to duty." }, |
| { id: 24, label: "Ashwatthama", group: "Teacher", title: "Son of Drona", desc: "Born with a gem on his forehead granting immortality. Committed a horrific night-massacre after the war." }, |
| { id: 25, label: "Shakuni", group: "Kaurava", title: "Prince of Gandhara", desc: "Brother of Gandhari. The mastermind behind the game of dice and the poisoning of the Kauravas' minds." }, |
| { id: 26, label: "Drupada", group: "Panchala", title: "King of Panchala", desc: "Father of Draupadi, Shikhandi, and Dhrishtadyumna. Sworn enemy of Drona." }, |
| { id: 27, label: "Shikhandi", group: "Panchala", title: "The Reborn Princess", desc: "Born Amba, reborn as a male warrior to fulfill a vow to kill Bhishma." }, |
| { id: 28, label: "Dhrishtadyumna", group: "Panchala", title: "Commander of Pandavas", desc: "Born from the same fire as Draupadi, destined to kill Drona." }, |
| { id: 29, label: "Abhimanyu", group: "Pandava", title: "The Tragic Hero", desc: "Son of Arjuna and Subhadra. Unjustly killed in the Chakravyuha formation by multiple Kaurava warriors." }, |
| { id: 30, label: "Subhadra", group: "Yadava", title: "Sister of Krishna", desc: "Wife of Arjuna, mother of Abhimanyu." }, |
| { id: 31, label: "Sanjaya", group: "Neutral", title: "The Narrator", desc: "Advisor and charioteer to Dhritarashtra. Granted divine vision by Vyasa to narrate the war." }, |
| { id: 32, label: "Kripacharya", group: "Teacher", title: "Kulaguru", desc: "The royal teacher of the Kurus before Drona. One of the few survivors of the war." }, |
| ]; |
| |
| const rawEdges = [ |
| |
| { from: 1, to: 3, label: "Parent", type: "Parent" }, |
| { from: 2, to: 3, label: "Parent", type: "Parent" }, |
| { from: 1, to: 4, label: "Spouse", type: "Spouse" }, |
| { from: 4, to: 5, label: "Parent", type: "Parent" }, |
| { from: 5, to: 6, label: "Parent", type: "Parent" }, |
| { from: 5, to: 7, label: "Parent", type: "Parent" }, |
| { from: 5, to: 8, label: "Parent", type: "Parent" }, |
| |
| |
| { from: 6, to: 7, label: "Sibling", type: "Sibling" }, |
| { from: 7, to: 8, label: "Sibling", type: "Sibling" }, |
| { from: 9, to: 25, label: "Sibling", type: "Sibling" }, |
| { from: 21, to: 22, label: "Sibling", type: "Sibling" }, |
| { from: 21, to: 30, label: "Sibling", type: "Sibling" }, |
| |
| |
| { from: 6, to: 9, label: "Spouse", type: "Spouse" }, |
| { from: 7, to: 10, label: "Spouse", type: "Spouse" }, |
| { from: 7, to: 11, label: "Spouse", type: "Spouse" }, |
| { from: 14, to: 30, label: "Spouse", type: "Spouse" }, |
| |
| |
| { from: 12, to: 20, label: "Spouse", type: "Spouse" }, |
| { from: 13, to: 20, label: "Spouse", type: "Spouse" }, |
| { from: 14, to: 20, label: "Spouse", type: "Spouse" }, |
| { from: 15, to: 20, label: "Spouse", type: "Spouse" }, |
| { from: 16, to: 20, label: "Spouse", type: "Spouse" }, |
| |
| |
| { from: 6, to: 17, label: "Parent", type: "Parent" }, |
| { from: 6, to: 18, label: "Parent", type: "Parent" }, |
| { from: 9, to: 17, label: "Parent", type: "Parent" }, |
| { from: 10, to: 12, label: "Parent", type: "Parent" }, |
| { from: 10, to: 13, label: "Parent", type: "Parent" }, |
| { from: 10, to: 14, label: "Parent", type: "Parent" }, |
| { from: 10, to: 19, label: "Parent", type: "Parent" }, |
| { from: 11, to: 15, label: "Parent", type: "Parent" }, |
| { from: 11, to: 16, label: "Parent", type: "Parent" }, |
| { from: 14, to: 29, label: "Parent", type: "Parent" }, |
| { from: 30, to: 29, label: "Parent", type: "Parent" }, |
| { from: 23, to: 24, label: "Parent", type: "Parent" }, |
| { from: 26, to: 20, label: "Parent", type: "Parent" }, |
| { from: 26, to: 27, label: "Parent", type: "Parent" }, |
| { from: 26, to: 28, label: "Parent", type: "Parent" }, |
| |
| |
| { from: 3, to: 6, label: "Guardian", type: "Teacher" }, |
| { from: 25, to: 17, label: "Mentor", type: "Teacher" }, |
| { from: 23, to: 14, label: "Teacher", type: "Teacher" }, |
| { from: 23, to: 17, label: "Teacher", type: "Teacher" }, |
| { from: 32, to: 14, label: "Teacher", type: "Teacher" }, |
| { from: 21, to: 14, label: "Guide/Friend", type: "Friend" }, |
| { from: 22, to: 13, label: "Teacher", type: "Teacher" }, |
| { from: 22, to: 17, label: "Teacher", type: "Teacher" }, |
| { from: 17, to: 19, label: "Friend/Ally", type: "Friend" }, |
| { from: 20, to: 21, label: "Friend", type: "Friend" }, |
| |
| |
| { from: 14, to: 19, label: "Rival", type: "Rival" }, |
| { from: 13, to: 17, label: "Rival", type: "Rival" }, |
| { from: 13, to: 18, label: "Rival", type: "Rival" }, |
| { from: 16, to: 25, label: "Rival", type: "Rival" }, |
| { from: 23, to: 26, label: "Rival", type: "Rival" }, |
| { from: 28, to: 23, label: "Killer", type: "Rival" }, |
| { from: 27, to: 3, label: "Killer", type: "Rival" }, |
| { from: 17, to: 12, label: "Rival", type: "Rival" } |
| ]; |
| |
| |
| const edges = new vis.DataSet(rawEdges.map(edge => { |
| const style = edgeStyles[edge.type] || edgeStyles.Default; |
| return { |
| ...edge, |
| color: style.color, |
| dashes: style.dashes, |
| font: { align: 'middle', size: 10, color: '#9ca3af', strokeWidth: 0 }, |
| arrows: 'to', |
| smooth: { type: 'continuous' } |
| }; |
| })); |
| |
| |
| const nodes = new vis.DataSet(rawNodes.map(node => ({ |
| ...node, |
| shape: 'dot', |
| size: 20, |
| font: { size: 14, color: '#f3f4f6', face: 'ui-sans-serif, system-ui, sans-serif' }, |
| borderWidth: 2, |
| borderWidthSelected: 4, |
| shadow: { enabled: true, color: 'rgba(0,0,0,0.5)', size: 10, x: 2, y: 2 } |
| }))); |
| |
| |
| const container = document.getElementById('network-container'); |
| const data = { nodes: nodes, edges: edges }; |
| const options = { |
| groups: groups, |
| nodes: { |
| scaling: { min: 10, max: 30 } |
| }, |
| edges: { |
| width: 1.5, |
| hoverWidth: 2, |
| selectionWidth: 3 |
| }, |
| physics: { |
| solver: 'barnesHut', |
| barnesHut: { |
| gravitationalConstant: -4000, |
| centralGravity: 0.3, |
| springLength: 150, |
| springConstant: 0.04, |
| damping: 0.09 |
| }, |
| stabilization: { |
| enabled: true, |
| iterations: 200, |
| updateInterval: 50 |
| } |
| }, |
| interaction: { |
| hover: true, |
| tooltipDelay: 200, |
| hideEdgesOnDrag: true |
| } |
| }; |
| |
| const network = new vis.Network(container, data, options); |
| |
| |
| |
| const sidebar = document.getElementById('sidebar'); |
| const closeSidebarBtn = document.getElementById('closeSidebar'); |
| const searchInput = document.getElementById('searchInput'); |
| const searchResults = document.getElementById('searchResults'); |
| let selectedNodeId = null; |
| |
| |
| function openSidebar(nodeId) { |
| const node = rawNodes.find(n => n.id === nodeId); |
| if (!node) return; |
| |
| selectedNodeId = nodeId; |
| |
| document.getElementById('charName').textContent = node.label; |
| document.getElementById('charTitle').textContent = node.title; |
| document.getElementById('charDesc').textContent = node.desc; |
| |
| const factionBadge = document.getElementById('charFaction'); |
| factionBadge.textContent = node.group; |
| factionBadge.style.backgroundColor = groups[node.group].color.background; |
| factionBadge.style.color = '#fff'; |
| |
| |
| const relationsList = document.getElementById('charRelations'); |
| relationsList.innerHTML = ''; |
| |
| const connectedEdges = rawEdges.filter(e => e.from === nodeId || e.to === nodeId); |
| |
| if (connectedEdges.length === 0) { |
| relationsList.innerHTML = '<li class="text-sm text-gray-500">No known relationships in database.</li>'; |
| } else { |
| connectedEdges.forEach(edge => { |
| const isSource = edge.from === nodeId; |
| const otherNodeId = isSource ? edge.to : edge.from; |
| const otherNode = rawNodes.find(n => n.id === otherNodeId); |
| |
| let relationText = edge.label; |
| if (edge.type === 'Parent') { |
| relationText = isSource ? 'Parent of' : 'Child of'; |
| } |
| |
| const li = document.createElement('li'); |
| li.className = "flex items-center justify-between p-3 bg-gray-800/50 rounded-lg border border-gray-700/50 hover:bg-gray-700/50 cursor-pointer transition-colors"; |
| li.innerHTML = ` |
| <div class="flex items-center gap-3"> |
| <div class="w-3 h-3 rounded-full" style="background-color: ${groups[otherNode.group].color.background}"></div> |
| <span class="font-medium text-gray-200">${otherNode.label}</span> |
| </div> |
| <span class="text-xs text-gray-400 font-medium px-2 py-1 bg-gray-900/50 rounded">${relationText}</span> |
| `; |
| |
| li.onclick = () => { |
| network.selectNodes([otherNodeId]); |
| openSidebar(otherNodeId); |
| focusOnNode(otherNodeId); |
| }; |
| relationsList.appendChild(li); |
| }); |
| } |
| |
| |
| sidebar.classList.remove('translate-x-full'); |
| } |
| |
| |
| function closeSidebar() { |
| sidebar.classList.add('translate-x-full'); |
| network.unselectAll(); |
| selectedNodeId = null; |
| } |
| |
| closeSidebarBtn.addEventListener('click', closeSidebar); |
| |
| |
| network.on('selectNode', function (params) { |
| openSidebar(params.nodes[0]); |
| }); |
| |
| network.on('deselectNode', function (params) { |
| closeSidebar(); |
| }); |
| |
| |
| function focusOnNode(nodeId) { |
| network.focus(nodeId, { |
| scale: 1.2, |
| animation: { duration: 1000, easingFunction: 'easeInOutQuad' } |
| }); |
| } |
| |
| document.getElementById('focusNodeBtn').addEventListener('click', () => { |
| if (selectedNodeId) focusOnNode(selectedNodeId); |
| }); |
| |
| |
| searchInput.addEventListener('input', (e) => { |
| const term = e.target.value.toLowerCase(); |
| searchResults.innerHTML = ''; |
| |
| if (term.length < 1) { |
| searchResults.classList.add('hidden'); |
| return; |
| } |
| |
| const matches = rawNodes.filter(n => |
| n.label.toLowerCase().includes(term) || |
| n.title.toLowerCase().includes(term) |
| ); |
| |
| if (matches.length > 0) { |
| searchResults.classList.remove('hidden'); |
| matches.forEach(match => { |
| const li = document.createElement('li'); |
| li.className = "px-4 py-3 hover:bg-gray-700 cursor-pointer border-b border-gray-700/50 last:border-0 flex justify-between items-center"; |
| li.innerHTML = ` |
| <div> |
| <div class="font-medium text-white">${match.label}</div> |
| <div class="text-xs text-gray-400">${match.title}</div> |
| </div> |
| <div class="w-2 h-2 rounded-full" style="background-color: ${groups[match.group].color.background}"></div> |
| `; |
| li.onclick = () => { |
| network.selectNodes([match.id]); |
| openSidebar(match.id); |
| focusOnNode(match.id); |
| searchResults.classList.add('hidden'); |
| searchInput.value = ''; |
| }; |
| searchResults.appendChild(li); |
| }); |
| } else { |
| searchResults.classList.remove('hidden'); |
| searchResults.innerHTML = '<li class="px-4 py-3 text-sm text-gray-500 text-center">No characters found</li>'; |
| } |
| }); |
| |
| |
| document.addEventListener('click', (e) => { |
| if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) { |
| searchResults.classList.add('hidden'); |
| } |
| }); |
| |
| |
| const legendContainer = document.getElementById('legendContainer'); |
| Object.keys(groups).forEach(group => { |
| const color = groups[group].color.background; |
| legendContainer.innerHTML += ` |
| <div class="flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity" onclick="filterByGroup('${group}')"> |
| <div class="w-3 h-3 rounded-full shadow-sm" style="background-color: ${color}"></div> |
| <span class="text-gray-300 hover:text-white">${group}</span> |
| </div> |
| `; |
| }); |
| |
| |
| let currentFilter = null; |
| window.filterByGroup = function(group) { |
| if (currentFilter === group) { |
| |
| nodes.forEach(n => nodes.update({id: n.id, hidden: false})); |
| edges.forEach(e => edges.update({id: e.id, hidden: false})); |
| currentFilter = null; |
| } else { |
| |
| nodes.forEach(n => { |
| nodes.update({id: n.id, hidden: n.group !== group}); |
| }); |
| |
| edges.forEach(e => { |
| const fromNode = nodes.get(e.from); |
| const toNode = nodes.get(e.to); |
| edges.update({id: e.id, hidden: fromNode.group !== group || toNode.group !== group}); |
| }); |
| currentFilter = group; |
| network.fit({animation: {duration: 1000}}); |
| } |
| }; |
| |
| |
| network.once("stabilizationIterationsDone", function() { |
| network.fit({ |
| animation: { |
| duration: 1500, |
| easingFunction: "easeOutQuint" |
| } |
| }); |
| }); |
| |
| </script> |
| </body> |
| </html> |