Graphinate 0.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,719 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Graphinate Viewer</title>
5
+ <link rel="modulepreload"
6
+ href="https://cdn.jsdelivr.net/npm/tweakpane@4.0.5/dist/tweakpane.min.js"
7
+ integrity="sha256-DN53dubYps0y0SjSHj8gO34VKtxaXqipuG/3jAJ2aWw="
8
+ crossorigin="anonymous">
9
+ <link rel="modulepreload"
10
+ href="https://cdn.jsdelivr.net/npm/@tweakpane/plugin-essentials@0.2.1/dist/tweakpane-plugin-essentials.min.js"
11
+ integrity="sha256-VYrWNKBoz1vsS026wJe4Zvb5/r8kGOSTfIBcmLdOA1g="
12
+ crossorigin="anonymous">
13
+ <script type="module" defer>
14
+ // Import ES module
15
+ import * as Tweakpane from 'https://cdn.jsdelivr.net/npm/tweakpane@4.0.5/dist/tweakpane.min.js';
16
+ import * as TweakpaneEssentialsPlugin
17
+ from 'https://cdn.jsdelivr.net/npm/@tweakpane/plugin-essentials@0.2.1/dist/tweakpane-plugin-essentials.min.js';
18
+ // Export it as a global variable
19
+ globalThis.Tweakpane = Tweakpane;
20
+ globalThis.TweakpaneEssentialsPlugin = TweakpaneEssentialsPlugin;
21
+ </script>
22
+ <script src="https://cdn.jsdelivr.net/npm/3d-force-graph@1.79.0/dist/3d-force-graph.min.js"
23
+ integrity="sha256-Khop08zFFZ8EobULFj/LkDMzwCaxIr/4WKcJZbLDjoc="
24
+ crossorigin="anonymous"></script>
25
+ <script src="https://cdn.jsdelivr.net/npm/murmurhash-js@1.0.0/murmurhash3_gc.min.js"
26
+ integrity="sha384-RTcg9S2mr/vVW+vsvQZB7G2cYWnhkBdsMvqKzHxL41KG4zTgrMSzGfm7mzw0aYU2"
27
+ crossorigin="anonymous"></script>
28
+ <!-- Append this element into the head element to apply the theme -->
29
+ <style>
30
+ body {
31
+ margin: 0;
32
+ }
33
+
34
+ .tp-dfwv {
35
+ height: 95vh;
36
+ min-width: 272px;
37
+ overflow-y: auto;
38
+ }
39
+
40
+ :root {
41
+ --tp-base-background-color: hsla(40, 3%, 90%, 1.00);
42
+ --tp-base-shadow-color: hsla(0, 0%, 0%, 0.30);
43
+ --tp-button-background-color: hsla(40, 3%, 70%, 1.00);
44
+ --tp-button-background-color-active: hsla(40, 3%, 55%, 1.00);
45
+ --tp-button-background-color-focus: hsla(40, 3%, 60%, 1.00);
46
+ --tp-button-background-color-hover: hsla(40, 3%, 65%, 1.00);
47
+ --tp-button-foreground-color: hsla(40, 3%, 20%, 1.00);
48
+ --tp-container-background-color: hsla(40, 3%, 70%, 1.00);
49
+ --tp-container-background-color-active: hsla(40, 3%, 55%, 1.00);
50
+ --tp-container-background-color-focus: hsla(40, 3%, 60%, 1.00);
51
+ --tp-container-background-color-hover: hsla(40, 3%, 65%, 1.00);
52
+ --tp-container-foreground-color: hsla(40, 3%, 20%, 1.00);
53
+ --tp-groove-foreground-color: hsla(40, 3%, 40%, 1.00);
54
+ --tp-input-background-color: hsla(120, 3%, 20%, 1.00);
55
+ --tp-input-background-color-active: hsla(120, 3%, 35%, 1.00);
56
+ --tp-input-background-color-focus: hsla(120, 3%, 30%, 1.00);
57
+ --tp-input-background-color-hover: hsla(120, 3%, 25%, 1.00);
58
+ --tp-input-foreground-color: hsla(120, 40%, 60%, 1.00);
59
+ --tp-label-foreground-color: hsla(40, 3%, 50%, 1.00);
60
+ --tp-monitor-background-color: hsla(120, 3%, 20%, 1.00);
61
+ --tp-monitor-foreground-color: hsla(120, 40%, 60%, 0.80);
62
+ }
63
+ </style>
64
+ </head>
65
+ <body>
66
+ <div id="3d-graph"></div>
67
+ <script type="module" lang="javascript">
68
+ import * as THREE from 'https://esm.sh/three';
69
+ import {scaleLinear, interpolateRgb, color as d3Color} from 'https://esm.sh/d3';
70
+
71
+ // region FloatingIFramePanel
72
+ let highestZIndex = 0;
73
+ let lastPanelPosition = {top: 50, left: 50};
74
+
75
+ function createFloatingIFramePanel(url, title) {
76
+ // Create the panel container
77
+ const panel = document.createElement('div');
78
+ panel.className = 'floating-panel';
79
+ panel.style.position = 'absolute';
80
+ panel.style.top = `${lastPanelPosition.top}px`;
81
+ panel.style.left = `${lastPanelPosition.left}px`;
82
+ panel.style.width = '800px';
83
+ panel.style.height = '600px';
84
+ panel.style.border = '4px solid var(--tp-container-background-color)';
85
+ panel.style.borderBottom = 'none'; // Remove bottom border
86
+ panel.style.borderRadius = '5px'; // Smaller rounded corners
87
+ panel.style.backgroundColor = '#fff';
88
+ panel.style.boxShadow = '0 2px 10px rgba(0,0,0,0.1)';
89
+ panel.style.resize = 'both';
90
+ panel.style.overflow = 'hidden'; // Prevent panel from scrolling
91
+ panel.style.zIndex = ++highestZIndex; // Set zIndex to one higher than the highest
92
+
93
+ // Update the last panel position
94
+ lastPanelPosition.top += 10;
95
+ lastPanelPosition.left += 10;
96
+
97
+ function lowerZIndexForHigherPanels(panel) {
98
+ const panels = document.querySelectorAll('.floating-panel');
99
+ for (const p of panels) {
100
+ const zIndex = Number.parseInt(globalThis.getComputedStyle(p).zIndex, 10);
101
+ if (zIndex > Number.parseInt(panel.style.zIndex, 10)) {
102
+ p.style.zIndex = zIndex - 1;
103
+ }
104
+ }
105
+ }
106
+
107
+ // Function to bring the panel to the front
108
+ function bringToFront() {
109
+ lowerZIndexForHigherPanels(panel);
110
+ // Set the current panel's zIndex to the highest
111
+ panel.style.zIndex = highestZIndex;
112
+ }
113
+
114
+ // Add event listener to bring the panel to the front on click
115
+ panel.addEventListener('mousedown', (e) => {
116
+ if (e.target !== closeButton && e.target !== maximizeToggleButton) {
117
+ bringToFront();
118
+ }
119
+ });
120
+
121
+ // Create the panel header
122
+ const header = document.createElement('div');
123
+ header.className = 'floating-panel-header';
124
+ header.style.width = '100%';
125
+ header.style.height = '20px'; // Set height to 20px
126
+ header.style.backgroundColor = '#b5b3b0'; // Updated background color
127
+ header.style.borderBottom = '1px solid #b5b3b0';
128
+ header.style.cursor = 'move';
129
+ header.style.display = 'flex';
130
+ header.style.justifyContent = 'space-between'; // Align items to the sides
131
+ header.style.alignItems = 'center';
132
+ header.style.padding = '0 5px'; // Adjust padding to fit buttons
133
+ header.style.boxSizing = 'border-box'; // Include padding in width calculation
134
+ header.style.position = 'relative';
135
+ header.style.zIndex = '1'; // Ensure header is above iframe
136
+
137
+ // Create the title element
138
+ const titleElement = document.createElement('div');
139
+ titleElement.className = 'floating-panel-title';
140
+ titleElement.innerHTML = title; // Support HTML syntax
141
+ titleElement.style.flexGrow = '1';
142
+ titleElement.style.textAlign = 'left'; // Justify title to the left
143
+ titleElement.style.fontFamily = 'monospace'; // Monospace font
144
+ titleElement.style.userSelect = 'none'; // Prevent text selection
145
+
146
+ // Create the loading ticker
147
+ const loadingTicker = document.createElement('div');
148
+ loadingTicker.innerHTML = 'Loading...';
149
+ loadingTicker.style.marginLeft = '10px';
150
+ loadingTicker.style.marginTop = '10px';
151
+ loadingTicker.style.display = 'none'; // Hide by default
152
+
153
+ // Create the button container
154
+ const buttonContainer = document.createElement('div');
155
+ buttonContainer.className = 'floating-panel-buttons';
156
+ buttonContainer.style.display = 'flex';
157
+ buttonContainer.style.gap = '5px';
158
+ buttonContainer.style.margin = '0'; // Remove margin to fit buttons
159
+
160
+ // Create the close button
161
+ const closeButton = document.createElement('button');
162
+ closeButton.className = 'floating-panel-close';
163
+ closeButton.innerHTML = '✖';
164
+ closeButton.style.border = 'none';
165
+ closeButton.style.background = 'none';
166
+ closeButton.style.cursor = 'pointer';
167
+ closeButton.style.fontSize = '12px';
168
+ closeButton.style.color = '#333';
169
+ closeButton.style.padding = '0 5px'; // Add padding to the button
170
+ closeButton.addEventListener('click', () => {
171
+ lowerZIndexForHigherPanels(panel);
172
+ highestZIndex--;
173
+ panel.remove();
174
+ });
175
+
176
+ // Create the maximize toggle button
177
+ const maximizeToggleButton = document.createElement('button');
178
+ maximizeToggleButton.className = 'floating-panel-maximize';
179
+ maximizeToggleButton.innerHTML = '🗖';
180
+ maximizeToggleButton.style.border = 'none';
181
+ maximizeToggleButton.style.background = 'none';
182
+ maximizeToggleButton.style.cursor = 'pointer';
183
+ maximizeToggleButton.style.fontSize = '12px';
184
+ maximizeToggleButton.style.color = '#333';
185
+ maximizeToggleButton.style.padding = '0 5px'; // Add padding to the button
186
+ let isMaximized = false;
187
+ let lastSize = {
188
+ width: panel.style.width,
189
+ height: panel.style.height,
190
+ top: panel.style.top,
191
+ left: panel.style.left
192
+ };
193
+
194
+ function toggleMaximize() {
195
+ bringToFront();
196
+ if (isMaximized) {
197
+ panel.style.width = lastSize.width;
198
+ panel.style.height = lastSize.height;
199
+ panel.style.top = lastSize.top;
200
+ panel.style.left = lastSize.left;
201
+ header.style.cursor = 'move';
202
+ maximizeToggleButton.innerHTML = '🗖';
203
+ } else {
204
+ lastSize = {
205
+ width: panel.style.width,
206
+ height: panel.style.height,
207
+ top: panel.style.top,
208
+ left: panel.style.left
209
+ };
210
+ panel.style.width = 'calc(100% - 10px - 8px)'; // padding + border
211
+ panel.style.height = 'calc(100% - 10px)';
212
+ panel.style.top = '5px';
213
+ panel.style.left = '5px';
214
+ header.style.cursor = 'default';
215
+ maximizeToggleButton.innerHTML = '❐';
216
+ }
217
+ isMaximized = !isMaximized;
218
+ }
219
+
220
+ // Add double-click event listener to the header to toggle maximize
221
+ header.addEventListener('dblclick', toggleMaximize);
222
+ maximizeToggleButton.addEventListener('click', toggleMaximize);
223
+
224
+ // Append buttons to the button container
225
+ buttonContainer.appendChild(maximizeToggleButton);
226
+ buttonContainer.appendChild(closeButton);
227
+
228
+ // Append the title element, loading ticker, and button container to the header
229
+ header.appendChild(titleElement);
230
+ header.appendChild(loadingTicker);
231
+ header.appendChild(buttonContainer);
232
+
233
+ // Create the iframe to load the URL
234
+ const iframe = document.createElement('iframe');
235
+ iframe.className = 'floating-panel-iframe';
236
+ iframe.src = url;
237
+ iframe.style.width = 'calc(100% - 8px)'; // Adjust width to match border
238
+ iframe.style.height = 'calc(100% - 20px)'; // Adjust height to match header
239
+ iframe.style.border = '4px solid #e6e6e5';
240
+ iframe.style.overflow = 'auto'; // Allow iframe to scroll
241
+
242
+ // Show loading ticker when iframe is loading
243
+ iframe.addEventListener('load', () => {
244
+ loadingTicker.style.display = 'none';
245
+ });
246
+ iframe.addEventListener('beforeunload', () => {
247
+ loadingTicker.style.display = 'block';
248
+ });
249
+
250
+ // Append the header and iframe to the panel
251
+ panel.appendChild(header);
252
+ panel.appendChild(iframe);
253
+
254
+ // Make the panel draggable
255
+ let isDragging = false;
256
+ let offsetX, offsetY;
257
+
258
+ function onMouseMove(e) {
259
+ if (isDragging) {
260
+ panel.style.left = `${e.clientX - offsetX}px`;
261
+ panel.style.top = `${e.clientY - offsetY}px`;
262
+ }
263
+ }
264
+
265
+ function onMouseUp() {
266
+ isDragging = false;
267
+ document.removeEventListener('mousemove', onMouseMove);
268
+ document.removeEventListener('mouseup', onMouseUp);
269
+ }
270
+
271
+ header.addEventListener('mousedown', (e) => {
272
+ if (!isMaximized) {
273
+ isDragging = true;
274
+ offsetX = e.clientX - panel.getBoundingClientRect().left;
275
+ offsetY = e.clientY - panel.getBoundingClientRect().top;
276
+ document.addEventListener('mousemove', onMouseMove);
277
+ document.addEventListener('mouseup', onMouseUp);
278
+ }
279
+ });
280
+
281
+ // Append the panel to the body
282
+ document.body.appendChild(panel);
283
+ }
284
+
285
+ // endregion FloatingIFramePanel
286
+
287
+ // region GraphQL
288
+
289
+ const graphQuery = `query GenericGraph{nodes{...Details} links: edges{source{...Details} target{...Details} ...Details}} fragment Details on GraphElement {id label type color}`;
290
+ const nodeTypesQuery = `query GraphTypes{graph{name nodeTypes: nodeTypeCounts{name count: value} edgeTypes: edgeTypeCounts{name count: value}}}`;
291
+
292
+ function fetchGraphQL(payload) {
293
+ return fetch(
294
+ '/graphql',
295
+ {
296
+ method: 'post',
297
+ headers: {Accept: 'application/json', 'Content-Type': 'application/json'},
298
+ body: JSON.stringify(payload),
299
+ credentials: 'include',
300
+ }).then((response) => response.json());
301
+ }
302
+
303
+ // endregion GraphQL
304
+
305
+ // region ForceGraph3D
306
+ const nodeTypeColor = {}
307
+ const nodeTypeVisibility = {};
308
+ let graphParams = {
309
+ nodeVal: 2,
310
+ linkWidth: 0,
311
+ useLinkColorGradient: false,
312
+ linkCurvature: 0,
313
+ linkCurveRotation: 0,
314
+ linkDirectionalArrowLength: 0, // 0 hides
315
+ linkDirectionalArrowRelPos: 1, // value between 0 [source] and 1 [target]
316
+ linkDirectionalParticles: 0,
317
+ linkDirectionalParticleSpeed: 0.002,
318
+ linkDirectionalParticleWidth: 1.5,
319
+ linkDirectionalParticleColor: '#ffff00',
320
+ }
321
+
322
+ function createLabel(gEl) {
323
+ return `<div style="color: ${gEl.color}; font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; outline-color: ${gEl.color}">${gEl.type}<br>'${gEl.label}'</div>`
324
+ }
325
+
326
+ const Graph = new ForceGraph3D(document.getElementById('3d-graph'))
327
+ .nodeColor('color')
328
+ .nodeVal(node => graphParams.nodeVal)
329
+ .nodeLabel(node => createLabel(node))
330
+ .linkWidth(link => graphParams.linkWidth)
331
+ .linkColor('color')
332
+ .linkLabel(link => createLabel(link))
333
+ .linkCurvature(link => graphParams.linkCurvature)
334
+ .linkCurveRotation(link => graphParams.linkCurveRotation)
335
+ .linkDirectionalArrowLength(link => graphParams.linkDirectionalArrowLength)
336
+ .linkDirectionalArrowRelPos(link => graphParams.linkDirectionalArrowRelPos)
337
+ .linkDirectionalParticles(link => graphParams.linkDirectionalParticles)
338
+ .linkDirectionalParticleSpeed(link => graphParams.linkDirectionalParticleSpeed / 10000)
339
+ .linkDirectionalParticleWidth(link => graphParams.linkDirectionalParticleWidth)
340
+ .linkDirectionalParticleColor(link => graphParams.linkDirectionalParticleColor)
341
+ .cooldownTicks(100);
342
+
343
+
344
+ if (graphParams.useLinkColorGradient) {
345
+ Graph
346
+ .linkThreeObject(link => {
347
+ // 2 (nodes) x 3 (r+g+b) bytes between [0, 1]
348
+ // For example:
349
+ // new Float32Array([
350
+ // 1, 0, 0, // source node: red
351
+ // 0, 1, 0 // target node: green
352
+ // ]);
353
+ const nodeColorScale = scaleLinear()
354
+ .domain([0, 1]) //Define the domain of the scale
355
+ .interpolate(interpolateRgb) // Use interpolateRgb to create a color scale
356
+ .range([link.colors.source, link.colors.target]); // Define the range of colors
357
+
358
+ const colors = new Float32Array([].concat(
359
+ ...[0, 1]
360
+ .map(nodeColorScale)
361
+ .map(d3Color)
362
+ .map(({r, g, b}) => [r, g, b].map(v => v / 255)
363
+ )));
364
+
365
+ const material = new THREE.LineBasicMaterial({vertexColors: true});
366
+ const geometry = new THREE.BufferGeometry();
367
+ geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(2 * 3), 3));
368
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
369
+
370
+ return new THREE.Line(geometry, material);
371
+ })
372
+ .linkPositionUpdate((line, {start, end}) => {
373
+ const startR = Graph.nodeRelSize();
374
+ const endR = Graph.nodeRelSize();
375
+ const lineLen = Math.sqrt(['x', 'y', 'z'].map(dim => Math.pow((end[dim] || 0) - (start[dim] || 0), 2)).reduce((acc, v) => acc + v, 0));
376
+
377
+ const linePos = line.geometry.getAttribute('position');
378
+
379
+ // calculate coordinate on the node's surface instead of center
380
+ linePos.set([startR / lineLen, 1 - endR / lineLen].flatMap(t =>
381
+ ['x', 'y', 'z'].map(dim => start[dim] + (end[dim] - start[dim]) * t)
382
+ ));
383
+ linePos.needsUpdate = true;
384
+ return true;
385
+ });
386
+ }
387
+
388
+
389
+ function updateGraph(gData) {
390
+ Graph.graphData(gData).zoomToFit(400);
391
+ }
392
+
393
+ function getVisible(gData) {
394
+ const visibleNodes = []
395
+ for (const node of gData.nodes) {
396
+ if (node.type in nodeTypeVisibility && nodeTypeVisibility[node.type]) {
397
+ visibleNodes.push(node);
398
+ }
399
+ }
400
+
401
+ const visibleLinks = [];
402
+ for (const link of gData.links) {
403
+ if (visibleNodes.find((node) => node.id === link.source.id) && visibleNodes.find((node) => node.id === link.target.id)) {
404
+ visibleLinks.push(link);
405
+ }
406
+ }
407
+
408
+ return {nodes: visibleNodes, links: visibleLinks};
409
+ }
410
+
411
+ function setAllNodeTypeVisibility(value) {
412
+ for (const key of Object.keys(nodeTypeVisibility)) {
413
+ nodeTypeVisibility[key] = value;
414
+ }
415
+ }
416
+
417
+ function updateNodeTypeColorMapping(nodes) {
418
+ for (const node of nodes) {
419
+ if (!nodeTypeColor[node.type]) {
420
+ nodeTypeColor[node.type] = node.color;
421
+ }
422
+ }
423
+ }
424
+
425
+ function refreshGraph(gData) {
426
+ const visibleGData = getVisible(gData);
427
+ updateGraph(visibleGData);
428
+ }
429
+
430
+ function createGraphControlPanel(graph, gData) {
431
+ updateNodeTypeColorMapping(gData.nodes);
432
+
433
+
434
+ const pane = new Tweakpane.Pane({
435
+ title: 'Control Panel',
436
+ expanded: true
437
+ });
438
+ pane.registerPlugin(TweakpaneEssentialsPlugin);
439
+
440
+
441
+ pane.addButton({title: 'Zoom to Fit'}).on('click', () => Graph.zoomToFit(400));
442
+
443
+
444
+ pane.addBlade({
445
+ view: 'buttongrid',
446
+ size: [2, 1],
447
+ cells: (x, y) => ({
448
+ title: [
449
+ ['All On', 'All Off'],
450
+ ][y][x],
451
+ }),
452
+ label: 'Visibility',
453
+ }).on('click', (ev) => {
454
+ switch (ev.cell.title) {
455
+ case 'All On': {
456
+ setAllNodeTypeVisibility(true);
457
+ updateGraph(gData);
458
+ pane.refresh();
459
+ break;
460
+ }
461
+ case 'All Off': {
462
+ setAllNodeTypeVisibility(false);
463
+ const visibleGData = getVisible(gData);
464
+ updateGraph(visibleGData);
465
+ pane.refresh();
466
+ break;
467
+ }
468
+ default:
469
+ console.log('Unknown action');
470
+ }
471
+ });
472
+ // pane.addButton({title: 'Visibility - All On'}).on('click', () => {
473
+ // setAllNodeTypeVisibility(true);
474
+ // updateGraph(gData);
475
+ // pane.refresh();
476
+ // });
477
+ // pane.addButton({title: 'Visibility - All Off'}).on('click', () => {
478
+ // setAllNodeTypeVisibility(false);
479
+ // const visibleGData = getVisible(gData);
480
+ // updateGraph(visibleGData);
481
+ // pane.refresh();
482
+ // });
483
+
484
+ pane.addBlade({view: 'separator'});
485
+
486
+ const tab = pane.addTab({
487
+ pages: [
488
+ {title: 'Legend'},
489
+ {title: 'Advanced'},
490
+ {title: 'Tools'}
491
+ ],
492
+ });
493
+
494
+ // Legend tab
495
+ const legendTab = tab.pages[0];
496
+
497
+ for (const nodeType of graph.nodeTypes) {
498
+ let info = `V: ${nodeType.count}`
499
+ const edgeType = graph.edgeTypes.find((t) => t.name === nodeType.name);
500
+ if (edgeType) {
501
+ info += `, E: ${edgeType.count}`;
502
+ }
503
+
504
+ const nodeTypeFolder = legendTab.addFolder({title: `${nodeType.name} [${info}]`});
505
+
506
+ nodeTypeFolder.addBinding(nodeTypeColor, nodeType.name, {label: 'Color'})
507
+ .on('change', (ev) => {
508
+ for (const node of gData.nodes) {
509
+ if (node.type === nodeType.name) {
510
+ node.color = ev.value;
511
+ }
512
+ }
513
+ refreshGraph(gData);
514
+ });
515
+
516
+ nodeTypeVisibility[nodeType.name] = true;
517
+ nodeTypeFolder.addBinding(nodeTypeVisibility, nodeType.name, {label: 'Visible'})
518
+ .on('change', (ev) => {
519
+ refreshGraph(gData);
520
+ });
521
+ }
522
+
523
+ // Advanced tab
524
+
525
+ const advancedTab = tab.pages[1];
526
+
527
+ // TODO: Complete Preset Implementation
528
+ // const presets = () => JSON.parse(localStorage.getItem('graphPresets')) || {};
529
+ //
530
+ // function savePreset(name, settings) {
531
+ // presets[name] = settings;
532
+ // localStorage.setItem('graphPresets', JSON.stringify(presets()));
533
+ // }
534
+ //
535
+ //
536
+ // const presetFolder = advancedTab.addFolder({title: 'Presets'});
537
+ //
538
+ // presetFolder.addBlade({
539
+ // view: 'buttongrid',
540
+ // size: [2, 1],
541
+ // cells: (x, y) => ({
542
+ // title: [
543
+ // ['Save', 'Delete'],
544
+ // ][y][x],
545
+ // }),
546
+ // label: 'Preset',
547
+ // }).on('click', (ev) => {
548
+ // switch (ev.cell.title) {
549
+ // case 'Save':
550
+ // const presetName = prompt('Enter preset name:');
551
+ // if (presetName) {
552
+ // savePreset(presetName, {...graphParams});
553
+ // updatePresetDropdown();
554
+ // }
555
+ // break;
556
+ // case 'Delete':
557
+ // const selectedPresetName = presetDropdown.text;
558
+ // if (selectedPresetName && presets()[selectedPresetName]) {
559
+ // const newPresets = presets()
560
+ // delete newPresets[selectedPresetName];
561
+ // localStorage.setItem('graphPresets', JSON.stringify(newPresets));
562
+ // updatePresetDropdown();
563
+ // }
564
+ // break;
565
+ // default:
566
+ // console.log('Unknown action');
567
+ // }
568
+ // });
569
+ //
570
+ //
571
+ // presetFolder.addButton({title: 'Save Preset'}).on('click', () => {
572
+ // const presetName = prompt('Enter preset name:');
573
+ // if (presetName) {
574
+ // savePreset(presetName, {...graphParams});
575
+ // updatePresetDropdown();
576
+ // }
577
+ // });
578
+ //
579
+ // const presetDropdown = presetFolder.addBlade({
580
+ // view: 'list',
581
+ // label: 'Presets',
582
+ // options: Object.entries(presets()).map(([k, v]) => {
583
+ // return {text: k, value: v};
584
+ // }
585
+ // ),
586
+ // value: ''
587
+ // }).on('change', (ev) => {
588
+ // Object.assign(graphParams, ev.value);
589
+ // pane.refresh();
590
+ // Graph.refresh();
591
+ // });
592
+ //
593
+ // function updatePresetDropdown() {
594
+ // presetDropdown.options = Object.entries(presets()).map(([k, v]) => {
595
+ // return {text: k, value: v};
596
+ // }
597
+ // );
598
+ // pane.refresh();
599
+ // }
600
+ //
601
+ // // Call updatePresetDropdown initially to populate the dropdown
602
+ // updatePresetDropdown();
603
+
604
+ const nodesFolder = advancedTab.addFolder({title: 'Nodes'});
605
+
606
+ nodesFolder.addBinding(graphParams, 'nodeVal', {label: 'Volume', step: 1, min: 0, max: 30})
607
+ .on('change', (ev) => {
608
+ Graph.refresh();
609
+ });
610
+
611
+ const edgesFolder = advancedTab.addFolder({title: 'Edges'});
612
+
613
+ edgesFolder.addBinding(graphParams, 'linkDirectionalArrowLength', {label: 'Arrow Length', min: 0, max: 5})
614
+ .on('change', (ev) => {
615
+ Graph.refresh();
616
+ });
617
+
618
+ edgesFolder.addBinding(graphParams, 'linkDirectionalArrowRelPos', {
619
+ label: 'Arrow Position',
620
+ options: {
621
+ beginning: 0,
622
+ middle: 0.5,
623
+ end: 1,
624
+ }
625
+ })
626
+ .on('change', (ev) => {
627
+ Graph.refresh();
628
+ });
629
+
630
+ edgesFolder.addBlade({view: 'separator'});
631
+
632
+ edgesFolder.addBinding(graphParams, 'linkCurvature', {label: 'Curvature', min: -5, max: 5})
633
+ .on('change', (ev) => {
634
+ Graph.refresh();
635
+ // refreshGraph(gData);
636
+ });
637
+
638
+ edgesFolder.addBinding(graphParams, 'linkCurveRotation', {label: 'Curve Rotation'})
639
+ .on('change', (ev) => {
640
+ Graph.refresh();
641
+ });
642
+
643
+ edgesFolder.addBlade({view: 'separator'});
644
+
645
+ edgesFolder.addBinding(graphParams, 'linkDirectionalParticles', {
646
+ label: 'Particles',
647
+ format: (v) => v.toFixed(0),
648
+ step: 1,
649
+ min: 0,
650
+ max: 10
651
+ })
652
+ .on('change', (ev) => {
653
+ Graph.refresh();
654
+ });
655
+
656
+ edgesFolder.addBinding(graphParams, 'linkDirectionalParticleWidth', {label: 'Particles Width', min: 0, max: 10})
657
+ .on('change', (ev) => {
658
+ Graph.refresh();
659
+ });
660
+
661
+ edgesFolder.addBinding(
662
+ graphParams,
663
+ 'linkDirectionalParticleSpeed',
664
+ {
665
+ label: 'Particles Speed',
666
+ format: (v) => v.toFixed(0),
667
+ step: 1,
668
+ min: 0,
669
+ max: 300,
670
+ })
671
+ .on('change', (ev) => {
672
+ Graph.refresh();
673
+ });
674
+
675
+ edgesFolder.addBinding(graphParams, 'linkDirectionalParticleColor', {label: 'Particles Color'})
676
+ .on('change', (ev) => {
677
+ Graph.refresh();
678
+ });
679
+
680
+
681
+ // Tools tab
682
+ const toolsTab = tab.pages[2];
683
+
684
+ toolsTab.addButton({title: 'GraphQL IDE'}).on('click', () => createFloatingIFramePanel('/graphql', 'GraphQL IDE'));
685
+ toolsTab.addButton({title: 'GraphiQL IDE'}).on('click', () => createFloatingIFramePanel('/graphiql', 'Graph<i>i</i>QL'));
686
+ toolsTab.addButton({title: 'GraphQL Voyager'}).on('click', () => createFloatingIFramePanel('/voyager', 'GraphQL Voyager'));
687
+ toolsTab.addButton({title: 'RapiDoc'}).on('click', () => createFloatingIFramePanel('/rapidoc', 'RapiDoc'));
688
+ toolsTab.addButton({title: 'Metrics'}).on('click', () => createFloatingIFramePanel('/metrics', 'Metrics'));
689
+ }
690
+
691
+ fetchGraphQL({query: graphQuery})
692
+ .then((responseJson) => responseJson.data)
693
+ .then((gData) => {
694
+ gData.nodes = gData.nodes.map((node) => {
695
+ node.id = murmurhash3_32_gc(node.id, 1);
696
+ return node;
697
+ });
698
+ gData.links = gData.links.map((link) => {
699
+ return {
700
+ source: murmurhash3_32_gc(link.source.id, 1),
701
+ target: murmurhash3_32_gc(link.target.id, 1),
702
+ color: link.color ? link.color : link.source.color,
703
+ colors: {source: link.source.color, target: link.target.color},
704
+ label: link.label,
705
+ type: link.type
706
+ };
707
+ });
708
+
709
+ fetchGraphQL({query: nodeTypesQuery})
710
+ .then((responseJson) => responseJson.data)
711
+ .then((data) => createGraphControlPanel(data.graph, gData));
712
+
713
+ updateGraph(gData);
714
+ });
715
+
716
+ // endregion ForceGraph3D
717
+ </script>
718
+ </body>
719
+ </html>