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.
- graphinate/__init__.py +18 -0
- graphinate/__main__.py +4 -0
- graphinate/builders/__init__.py +55 -0
- graphinate/builders/_builder.py +58 -0
- graphinate/builders/_d3.py +61 -0
- graphinate/builders/_graphql.py +521 -0
- graphinate/builders/_mermaid.py +45 -0
- graphinate/builders/_networkx.py +227 -0
- graphinate/cli.py +123 -0
- graphinate/color.py +100 -0
- graphinate/constants.py +4 -0
- graphinate/converters.py +94 -0
- graphinate/enums.py +44 -0
- graphinate/modeling.py +337 -0
- graphinate/renderers/__init__.py +5 -0
- graphinate/renderers/graphql.py +111 -0
- graphinate/renderers/matplotlib.py +82 -0
- graphinate/server/__init__.py +0 -0
- graphinate/server/starlette/__init__.py +31 -0
- graphinate/server/starlette/views.py +17 -0
- graphinate/server/web/__init__.py +25 -0
- graphinate/server/web/elements/index.html +23 -0
- graphinate/server/web/graphiql/index.html +160 -0
- graphinate/server/web/rapidoc/index.html +17 -0
- graphinate/server/web/static/images/logo-128.png +0 -0
- graphinate/server/web/static/images/logo.svg +50 -0
- graphinate/server/web/static/images/network_graph.png +0 -0
- graphinate/server/web/viewer/index.html +719 -0
- graphinate/server/web/voyager/index.html +55 -0
- graphinate/tools.py +7 -0
- graphinate/typing.py +83 -0
- graphinate-0.12.0.dist-info/METADATA +284 -0
- graphinate-0.12.0.dist-info/RECORD +36 -0
- graphinate-0.12.0.dist-info/WHEEL +4 -0
- graphinate-0.12.0.dist-info/entry_points.txt +2 -0
- graphinate-0.12.0.dist-info/licenses/LICENSE +165 -0
|
@@ -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>
|