trysem commited on
Commit
e4389f2
·
verified ·
1 Parent(s): 764fcf8

Create kurugraph

Browse files
Files changed (1) hide show
  1. kurugraph +510 -0
kurugraph ADDED
@@ -0,0 +1,510 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Kuru-Graph: Mahabharata Network</title>
7
+
8
+ <!-- Tailwind CSS -->
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+
11
+ <!-- Vis-Network for Graph Visualization -->
12
+ <script type="text/javascript" src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
13
+
14
+ <!-- Lucide Icons -->
15
+ <script src="https://unpkg.com/lucide@latest"></script>
16
+
17
+ <style>
18
+ body { margin: 0; padding: 0; overflow: hidden; background-color: #111827; color: white; }
19
+ #network-container { width: 100vw; height: 100vh; }
20
+
21
+ /* Remove Vis.js default focus border */
22
+ .vis-network:focus { outline: none; }
23
+
24
+ /* Custom Scrollbar for sidebar */
25
+ ::-webkit-scrollbar { width: 6px; }
26
+ ::-webkit-scrollbar-track { background: #1f2937; }
27
+ ::-webkit-scrollbar-thumb { background: #4b5563; border-radius: 4px; }
28
+ ::-webkit-scrollbar-thumb:hover { background: #6b7280; }
29
+
30
+ .glass-panel {
31
+ background: rgba(31, 41, 55, 0.85);
32
+ backdrop-filter: blur(12px);
33
+ -webkit-backdrop-filter: blur(12px);
34
+ border: 1px solid rgba(75, 85, 99, 0.4);
35
+ }
36
+ </style>
37
+ </head>
38
+ <body class="font-sans antialiased text-gray-100 selection:bg-indigo-500 selection:text-white">
39
+
40
+ <!-- Graph Container -->
41
+ <div id="network-container" class="absolute inset-0 z-0"></div>
42
+
43
+ <!-- Top UI Bar -->
44
+ <div class="absolute top-0 left-0 w-full p-4 md:p-6 z-10 pointer-events-none flex justify-between items-start">
45
+ <div class="pointer-events-auto">
46
+ <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">
47
+ <i data-lucide="network"></i> Kuru-Graph
48
+ </h1>
49
+ <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>
50
+ </div>
51
+
52
+ <!-- Search Bar -->
53
+ <div class="pointer-events-auto relative mt-2 md:mt-0">
54
+ <div class="relative">
55
+ <i data-lucide="search" class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4"></i>
56
+ <input type="text" id="searchInput" placeholder="Search character..."
57
+ 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">
58
+ </div>
59
+ <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">
60
+ <!-- Results injected here -->
61
+ </ul>
62
+ </div>
63
+ </div>
64
+
65
+ <!-- Legend -->
66
+ <div class="absolute bottom-6 left-6 z-10 pointer-events-auto glass-panel p-4 rounded-xl shadow-2xl hidden md:block">
67
+ <h3 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-3">Factions</h3>
68
+ <div class="grid grid-cols-2 gap-x-6 gap-y-2 text-sm" id="legendContainer">
69
+ <!-- Legend injected here -->
70
+ </div>
71
+ <div class="mt-4 pt-3 border-t border-gray-700">
72
+ <h3 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-2">Relationships</h3>
73
+ <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>
74
+ <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>
75
+ <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>
76
+ <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>
77
+ </div>
78
+ </div>
79
+
80
+ <!-- Sidebar (Character Details) -->
81
+ <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">
82
+
83
+ <!-- Header -->
84
+ <div class="p-6 pb-4 border-b border-gray-700 flex justify-between items-start relative">
85
+ <div>
86
+ <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>
87
+ <h2 id="charName" class="text-3xl font-bold text-white mb-1">Character Name</h2>
88
+ <p id="charTitle" class="text-amber-400 text-sm italic">Title / Role</p>
89
+ </div>
90
+ <button id="closeSidebar" class="p-2 text-gray-400 hover:text-white hover:bg-gray-700 rounded-full transition-colors">
91
+ <i data-lucide="x" class="w-5 h-5"></i>
92
+ </button>
93
+ </div>
94
+
95
+ <!-- Body -->
96
+ <div class="p-6 flex-1 overflow-y-auto">
97
+ <h3 class="text-lg font-semibold border-b border-gray-700 pb-2 mb-3">About</h3>
98
+ <p id="charDesc" class="text-gray-300 text-sm leading-relaxed mb-6">
99
+ Character description goes here.
100
+ </p>
101
+
102
+ <h3 class="text-lg font-semibold border-b border-gray-700 pb-2 mb-3">Key Relationships</h3>
103
+ <ul id="charRelations" class="space-y-3">
104
+ <!-- Relations injected here -->
105
+ </ul>
106
+ </div>
107
+
108
+ <!-- Footer actions -->
109
+ <div class="p-4 border-t border-gray-700 bg-gray-800/50">
110
+ <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">
111
+ <i data-lucide="target" class="w-4 h-4"></i> Center in Graph
112
+ </button>
113
+ </div>
114
+ </div>
115
+
116
+ <!-- Mobile Legend Toggle -->
117
+ <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">
118
+ <i data-lucide="info" class="w-6 h-6"></i>
119
+ </button>
120
+
121
+ <script>
122
+ // Initialize Icons
123
+ lucide.createIcons();
124
+
125
+ // --- Data Definition ---
126
+
127
+ // Faction Colors & Styling
128
+ const groups = {
129
+ Pandava: { color: { background: '#2563eb', border: '#1d4ed8', hover: { background: '#3b82f6', border: '#2563eb' } } },
130
+ Kaurava: { color: { background: '#dc2626', border: '#b91c1c', hover: { background: '#ef4444', border: '#dc2626' } } },
131
+ Elder: { color: { background: '#d97706', border: '#b45309', hover: { background: '#f59e0b', border: '#d97706' } } },
132
+ Yadava: { color: { background: '#059669', border: '#047857', hover: { background: '#10b981', border: '#059669' } } },
133
+ Teacher: { color: { background: '#7c3aed', border: '#6d28d9', hover: { background: '#8b5cf6', border: '#7c3aed' } } },
134
+ Panchala: { color: { background: '#db2777', border: '#be185d', hover: { background: '#ec4899', border: '#db2777' } } },
135
+ Divine: { color: { background: '#0891b2', border: '#0e7490', hover: { background: '#06b6d4', border: '#0891b2' } } },
136
+ Neutral: { color: { background: '#4b5563', border: '#374151', hover: { background: '#6b7280', border: '#4b5563' } } }
137
+ };
138
+
139
+ // Edge Types Styling
140
+ const edgeStyles = {
141
+ Parent: { color: '#9ca3af', dashes: false },
142
+ Spouse: { color: '#ec4899', dashes: false },
143
+ Rival: { color: '#ef4444', dashes: [5, 5] },
144
+ Teacher: { color: '#a855f7', dashes: false },
145
+ Friend: { color: '#10b981', dashes: false },
146
+ Sibling: { color: '#60a5fa', dashes: false },
147
+ Default: { color: '#6b7280', dashes: false }
148
+ };
149
+
150
+ // Character Database
151
+ const rawNodes = [
152
+ { id: 1, label: "Shantanu", group: "Elder", title: "King of Hastinapur", desc: "A great king of the Kuru dynasty, ancestor to both Pandavas and Kauravas." },
153
+ { id: 2, label: "Ganga", group: "Divine", title: "River Goddess", desc: "First wife of Shantanu, mother of Bhishma." },
154
+ { 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." },
155
+ { id: 4, label: "Satyavati", group: "Elder", title: "Queen Mother", desc: "Second wife of Shantanu. Mother of Vyasa, Chitrangada, and Vichitravirya." },
156
+ { 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." },
157
+ { 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." },
158
+ { 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." },
159
+ { id: 8, label: "Vidura", group: "Elder", title: "Prime Minister", desc: "Incarnation of Yama (Dharma). Known for his unparalleled wisdom and adherence to righteousness." },
160
+ { id: 9, label: "Gandhari", group: "Kaurava", title: "Queen of Hastinapur", desc: "Wife of Dhritarashtra. Blindfolded herself out of solidarity with her blind husband." },
161
+ { 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." },
162
+ { 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." },
163
+ { 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." },
164
+ { 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." },
165
+ { 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." },
166
+ { 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." },
167
+ { id: 16, label: "Sahadeva", group: "Pandava", title: "The Scholar", desc: "Son of the Ashvins. A master of astrology and swordsmanship. Swore to kill Shakuni." },
168
+ { id: 17, label: "Duryodhana", group: "Kaurava", title: "Eldest Kaurava", desc: "The primary antagonist. Driven by envy and hatred toward his cousins, the Pandavas." },
169
+ { id: 18, label: "Dushasana", group: "Kaurava", title: "Second Kaurava", desc: "Duryodhana's loyal brother. Infamous for dragging Draupadi by her hair." },
170
+ { 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." },
171
+ { 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." },
172
+ { 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." },
173
+ { 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." },
174
+ { 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." },
175
+ { 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." },
176
+ { 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." },
177
+ { id: 26, label: "Drupada", group: "Panchala", title: "King of Panchala", desc: "Father of Draupadi, Shikhandi, and Dhrishtadyumna. Sworn enemy of Drona." },
178
+ { 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." },
179
+ { id: 28, label: "Dhrishtadyumna", group: "Panchala", title: "Commander of Pandavas", desc: "Born from the same fire as Draupadi, destined to kill Drona." },
180
+ { 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." },
181
+ { id: 30, label: "Subhadra", group: "Yadava", title: "Sister of Krishna", desc: "Wife of Arjuna, mother of Abhimanyu." },
182
+ { id: 31, label: "Sanjaya", group: "Neutral", title: "The Narrator", desc: "Advisor and charioteer to Dhritarashtra. Granted divine vision by Vyasa to narrate the war." },
183
+ { 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." },
184
+ ];
185
+
186
+ const rawEdges = [
187
+ // Lineage
188
+ { from: 1, to: 3, label: "Parent", type: "Parent" },
189
+ { from: 2, to: 3, label: "Parent", type: "Parent" },
190
+ { from: 1, to: 4, label: "Spouse", type: "Spouse" },
191
+ { from: 4, to: 5, label: "Parent", type: "Parent" },
192
+ { from: 5, to: 6, label: "Parent", type: "Parent" },
193
+ { from: 5, to: 7, label: "Parent", type: "Parent" },
194
+ { from: 5, to: 8, label: "Parent", type: "Parent" },
195
+
196
+ // Siblings Generation 1
197
+ { from: 6, to: 7, label: "Sibling", type: "Sibling" },
198
+ { from: 7, to: 8, label: "Sibling", type: "Sibling" },
199
+ { from: 9, to: 25, label: "Sibling", type: "Sibling" },
200
+ { from: 21, to: 22, label: "Sibling", type: "Sibling" },
201
+ { from: 21, to: 30, label: "Sibling", type: "Sibling" },
202
+
203
+ // Marriages
204
+ { from: 6, to: 9, label: "Spouse", type: "Spouse" },
205
+ { from: 7, to: 10, label: "Spouse", type: "Spouse" },
206
+ { from: 7, to: 11, label: "Spouse", type: "Spouse" },
207
+ { from: 14, to: 30, label: "Spouse", type: "Spouse" },
208
+
209
+ // Draupadi
210
+ { from: 12, to: 20, label: "Spouse", type: "Spouse" },
211
+ { from: 13, to: 20, label: "Spouse", type: "Spouse" },
212
+ { from: 14, to: 20, label: "Spouse", type: "Spouse" },
213
+ { from: 15, to: 20, label: "Spouse", type: "Spouse" },
214
+ { from: 16, to: 20, label: "Spouse", type: "Spouse" },
215
+
216
+ // Offspring
217
+ { from: 6, to: 17, label: "Parent", type: "Parent" },
218
+ { from: 6, to: 18, label: "Parent", type: "Parent" },
219
+ { from: 9, to: 17, label: "Parent", type: "Parent" },
220
+ { from: 10, to: 12, label: "Parent", type: "Parent" },
221
+ { from: 10, to: 13, label: "Parent", type: "Parent" },
222
+ { from: 10, to: 14, label: "Parent", type: "Parent" },
223
+ { from: 10, to: 19, label: "Parent", type: "Parent" }, // Kunti -> Karna
224
+ { from: 11, to: 15, label: "Parent", type: "Parent" },
225
+ { from: 11, to: 16, label: "Parent", type: "Parent" },
226
+ { from: 14, to: 29, label: "Parent", type: "Parent" },
227
+ { from: 30, to: 29, label: "Parent", type: "Parent" },
228
+ { from: 23, to: 24, label: "Parent", type: "Parent" },
229
+ { from: 26, to: 20, label: "Parent", type: "Parent" },
230
+ { from: 26, to: 27, label: "Parent", type: "Parent" },
231
+ { from: 26, to: 28, label: "Parent", type: "Parent" },
232
+
233
+ // Mentorships / Loyalty
234
+ { from: 3, to: 6, label: "Guardian", type: "Teacher" },
235
+ { from: 25, to: 17, label: "Mentor", type: "Teacher" },
236
+ { from: 23, to: 14, label: "Teacher", type: "Teacher" },
237
+ { from: 23, to: 17, label: "Teacher", type: "Teacher" },
238
+ { from: 32, to: 14, label: "Teacher", type: "Teacher" },
239
+ { from: 21, to: 14, label: "Guide/Friend", type: "Friend" },
240
+ { from: 22, to: 13, label: "Teacher", type: "Teacher" },
241
+ { from: 22, to: 17, label: "Teacher", type: "Teacher" },
242
+ { from: 17, to: 19, label: "Friend/Ally", type: "Friend" },
243
+ { from: 20, to: 21, label: "Friend", type: "Friend" },
244
+
245
+ // Rivalries
246
+ { from: 14, to: 19, label: "Rival", type: "Rival" },
247
+ { from: 13, to: 17, label: "Rival", type: "Rival" },
248
+ { from: 13, to: 18, label: "Rival", type: "Rival" },
249
+ { from: 16, to: 25, label: "Rival", type: "Rival" },
250
+ { from: 23, to: 26, label: "Rival", type: "Rival" },
251
+ { from: 28, to: 23, label: "Killer", type: "Rival" },
252
+ { from: 27, to: 3, label: "Killer", type: "Rival" },
253
+ { from: 17, to: 12, label: "Rival", type: "Rival" }
254
+ ];
255
+
256
+ // Format Edges for Vis.js
257
+ const edges = new vis.DataSet(rawEdges.map(edge => {
258
+ const style = edgeStyles[edge.type] || edgeStyles.Default;
259
+ return {
260
+ ...edge,
261
+ color: style.color,
262
+ dashes: style.dashes,
263
+ font: { align: 'middle', size: 10, color: '#9ca3af', strokeWidth: 0 },
264
+ arrows: 'to',
265
+ smooth: { type: 'continuous' }
266
+ };
267
+ }));
268
+
269
+ // Format Nodes for Vis.js
270
+ const nodes = new vis.DataSet(rawNodes.map(node => ({
271
+ ...node,
272
+ shape: 'dot',
273
+ size: 20,
274
+ font: { size: 14, color: '#f3f4f6', face: 'ui-sans-serif, system-ui, sans-serif' },
275
+ borderWidth: 2,
276
+ borderWidthSelected: 4,
277
+ shadow: { enabled: true, color: 'rgba(0,0,0,0.5)', size: 10, x: 2, y: 2 }
278
+ })));
279
+
280
+ // --- Network Initialization ---
281
+ const container = document.getElementById('network-container');
282
+ const data = { nodes: nodes, edges: edges };
283
+ const options = {
284
+ groups: groups,
285
+ nodes: {
286
+ scaling: { min: 10, max: 30 }
287
+ },
288
+ edges: {
289
+ width: 1.5,
290
+ hoverWidth: 2,
291
+ selectionWidth: 3
292
+ },
293
+ physics: {
294
+ solver: 'barnesHut',
295
+ barnesHut: {
296
+ gravitationalConstant: -4000,
297
+ centralGravity: 0.3,
298
+ springLength: 150,
299
+ springConstant: 0.04,
300
+ damping: 0.09
301
+ },
302
+ stabilization: {
303
+ enabled: true,
304
+ iterations: 200,
305
+ updateInterval: 50
306
+ }
307
+ },
308
+ interaction: {
309
+ hover: true,
310
+ tooltipDelay: 200,
311
+ hideEdgesOnDrag: true
312
+ }
313
+ };
314
+
315
+ const network = new vis.Network(container, data, options);
316
+
317
+ // --- UI Interactions ---
318
+
319
+ const sidebar = document.getElementById('sidebar');
320
+ const closeSidebarBtn = document.getElementById('closeSidebar');
321
+ const searchInput = document.getElementById('searchInput');
322
+ const searchResults = document.getElementById('searchResults');
323
+ let selectedNodeId = null;
324
+
325
+ // Open Sidebar Function
326
+ function openSidebar(nodeId) {
327
+ const node = rawNodes.find(n => n.id === nodeId);
328
+ if (!node) return;
329
+
330
+ selectedNodeId = nodeId;
331
+
332
+ document.getElementById('charName').textContent = node.label;
333
+ document.getElementById('charTitle').textContent = node.title;
334
+ document.getElementById('charDesc').textContent = node.desc;
335
+
336
+ const factionBadge = document.getElementById('charFaction');
337
+ factionBadge.textContent = node.group;
338
+ factionBadge.style.backgroundColor = groups[node.group].color.background;
339
+ factionBadge.style.color = '#fff';
340
+
341
+ // Find Relations
342
+ const relationsList = document.getElementById('charRelations');
343
+ relationsList.innerHTML = '';
344
+
345
+ const connectedEdges = rawEdges.filter(e => e.from === nodeId || e.to === nodeId);
346
+
347
+ if (connectedEdges.length === 0) {
348
+ relationsList.innerHTML = '<li class="text-sm text-gray-500">No known relationships in database.</li>';
349
+ } else {
350
+ connectedEdges.forEach(edge => {
351
+ const isSource = edge.from === nodeId;
352
+ const otherNodeId = isSource ? edge.to : edge.from;
353
+ const otherNode = rawNodes.find(n => n.id === otherNodeId);
354
+
355
+ let relationText = edge.label;
356
+ if (edge.type === 'Parent') {
357
+ relationText = isSource ? 'Parent of' : 'Child of';
358
+ }
359
+
360
+ const li = document.createElement('li');
361
+ 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";
362
+ li.innerHTML = `
363
+ <div class="flex items-center gap-3">
364
+ <div class="w-3 h-3 rounded-full" style="background-color: ${groups[otherNode.group].color.background}"></div>
365
+ <span class="font-medium text-gray-200">${otherNode.label}</span>
366
+ </div>
367
+ <span class="text-xs text-gray-400 font-medium px-2 py-1 bg-gray-900/50 rounded">${relationText}</span>
368
+ `;
369
+ // Clicking relation opens that character
370
+ li.onclick = () => {
371
+ network.selectNodes([otherNodeId]);
372
+ openSidebar(otherNodeId);
373
+ focusOnNode(otherNodeId);
374
+ };
375
+ relationsList.appendChild(li);
376
+ });
377
+ }
378
+
379
+ // Show Sidebar
380
+ sidebar.classList.remove('translate-x-full');
381
+ }
382
+
383
+ // Close Sidebar
384
+ function closeSidebar() {
385
+ sidebar.classList.add('translate-x-full');
386
+ network.unselectAll();
387
+ selectedNodeId = null;
388
+ }
389
+
390
+ closeSidebarBtn.addEventListener('click', closeSidebar);
391
+
392
+ // Network Events
393
+ network.on('selectNode', function (params) {
394
+ openSidebar(params.nodes[0]);
395
+ });
396
+
397
+ network.on('deselectNode', function (params) {
398
+ closeSidebar();
399
+ });
400
+
401
+ // Focus Button
402
+ function focusOnNode(nodeId) {
403
+ network.focus(nodeId, {
404
+ scale: 1.2,
405
+ animation: { duration: 1000, easingFunction: 'easeInOutQuad' }
406
+ });
407
+ }
408
+
409
+ document.getElementById('focusNodeBtn').addEventListener('click', () => {
410
+ if (selectedNodeId) focusOnNode(selectedNodeId);
411
+ });
412
+
413
+ // Search Functionality
414
+ searchInput.addEventListener('input', (e) => {
415
+ const term = e.target.value.toLowerCase();
416
+ searchResults.innerHTML = '';
417
+
418
+ if (term.length < 1) {
419
+ searchResults.classList.add('hidden');
420
+ return;
421
+ }
422
+
423
+ const matches = rawNodes.filter(n =>
424
+ n.label.toLowerCase().includes(term) ||
425
+ n.title.toLowerCase().includes(term)
426
+ );
427
+
428
+ if (matches.length > 0) {
429
+ searchResults.classList.remove('hidden');
430
+ matches.forEach(match => {
431
+ const li = document.createElement('li');
432
+ 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";
433
+ li.innerHTML = `
434
+ <div>
435
+ <div class="font-medium text-white">${match.label}</div>
436
+ <div class="text-xs text-gray-400">${match.title}</div>
437
+ </div>
438
+ <div class="w-2 h-2 rounded-full" style="background-color: ${groups[match.group].color.background}"></div>
439
+ `;
440
+ li.onclick = () => {
441
+ network.selectNodes([match.id]);
442
+ openSidebar(match.id);
443
+ focusOnNode(match.id);
444
+ searchResults.classList.add('hidden');
445
+ searchInput.value = '';
446
+ };
447
+ searchResults.appendChild(li);
448
+ });
449
+ } else {
450
+ searchResults.classList.remove('hidden');
451
+ searchResults.innerHTML = '<li class="px-4 py-3 text-sm text-gray-500 text-center">No characters found</li>';
452
+ }
453
+ });
454
+
455
+ // Close search results on outside click
456
+ document.addEventListener('click', (e) => {
457
+ if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
458
+ searchResults.classList.add('hidden');
459
+ }
460
+ });
461
+
462
+ // Populate Legend
463
+ const legendContainer = document.getElementById('legendContainer');
464
+ Object.keys(groups).forEach(group => {
465
+ const color = groups[group].color.background;
466
+ legendContainer.innerHTML += `
467
+ <div class="flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity" onclick="filterByGroup('${group}')">
468
+ <div class="w-3 h-3 rounded-full shadow-sm" style="background-color: ${color}"></div>
469
+ <span class="text-gray-300 hover:text-white">${group}</span>
470
+ </div>
471
+ `;
472
+ });
473
+
474
+ // Simple Filter highlight
475
+ let currentFilter = null;
476
+ window.filterByGroup = function(group) {
477
+ if (currentFilter === group) {
478
+ // Reset
479
+ nodes.forEach(n => nodes.update({id: n.id, hidden: false}));
480
+ edges.forEach(e => edges.update({id: e.id, hidden: false}));
481
+ currentFilter = null;
482
+ } else {
483
+ // Filter
484
+ nodes.forEach(n => {
485
+ nodes.update({id: n.id, hidden: n.group !== group});
486
+ });
487
+ // Hide edges if either node is hidden
488
+ edges.forEach(e => {
489
+ const fromNode = nodes.get(e.from);
490
+ const toNode = nodes.get(e.to);
491
+ edges.update({id: e.id, hidden: fromNode.group !== group || toNode.group !== group});
492
+ });
493
+ currentFilter = group;
494
+ network.fit({animation: {duration: 1000}});
495
+ }
496
+ };
497
+
498
+ // Zoom out slightly on initial load
499
+ network.once("stabilizationIterationsDone", function() {
500
+ network.fit({
501
+ animation: {
502
+ duration: 1500,
503
+ easingFunction: "easeOutQuint"
504
+ }
505
+ });
506
+ });
507
+
508
+ </script>
509
+ </body>
510
+ </html>