superlocalmemory 2.6.0 → 2.7.0
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.
- package/CHANGELOG.md +167 -1803
- package/README.md +212 -397
- package/bin/slm +179 -3
- package/bin/superlocalmemoryv2:learning +4 -0
- package/bin/superlocalmemoryv2:patterns +4 -0
- package/docs/ACCESSIBILITY.md +291 -0
- package/docs/ARCHITECTURE.md +12 -6
- package/docs/FRAMEWORK-INTEGRATIONS.md +300 -0
- package/docs/MCP-MANUAL-SETUP.md +14 -4
- package/install.sh +99 -3
- package/mcp_server.py +291 -1
- package/package.json +2 -1
- package/requirements-learning.txt +12 -0
- package/scripts/verify-v27.sh +233 -0
- package/skills/slm-show-patterns/SKILL.md +224 -0
- package/src/learning/__init__.py +201 -0
- package/src/learning/adaptive_ranker.py +826 -0
- package/src/learning/cross_project_aggregator.py +866 -0
- package/src/learning/engagement_tracker.py +638 -0
- package/src/learning/feature_extractor.py +461 -0
- package/src/learning/feedback_collector.py +690 -0
- package/src/learning/learning_db.py +842 -0
- package/src/learning/project_context_manager.py +582 -0
- package/src/learning/source_quality_scorer.py +685 -0
- package/src/learning/synthetic_bootstrap.py +1047 -0
- package/src/learning/tests/__init__.py +0 -0
- package/src/learning/tests/test_adaptive_ranker.py +328 -0
- package/src/learning/tests/test_aggregator.py +309 -0
- package/src/learning/tests/test_feedback_collector.py +295 -0
- package/src/learning/tests/test_learning_db.py +606 -0
- package/src/learning/tests/test_project_context.py +296 -0
- package/src/learning/tests/test_source_quality.py +355 -0
- package/src/learning/tests/test_synthetic_bootstrap.py +433 -0
- package/src/learning/tests/test_workflow_miner.py +322 -0
- package/src/learning/workflow_pattern_miner.py +665 -0
- package/ui/index.html +346 -13
- package/ui/js/clusters.js +90 -1
- package/ui/js/graph-core.js +445 -0
- package/ui/js/graph-cytoscape-monolithic-backup.js +1168 -0
- package/ui/js/graph-cytoscape.js +1168 -0
- package/ui/js/graph-d3-backup.js +32 -0
- package/ui/js/graph-filters.js +220 -0
- package/ui/js/graph-interactions.js +354 -0
- package/ui/js/graph-ui.js +214 -0
- package/ui/js/memories.js +52 -0
- package/ui/js/modal.js +104 -1
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
// SuperLocalMemory V2.6.5 - Interactive Knowledge Graph - Core Rendering Module
|
|
2
|
+
// Copyright (c) 2026 Varun Pratap Bhardwaj — MIT License
|
|
3
|
+
// Part of modular graph visualization system (split from monolithic graph-cytoscape.js)
|
|
4
|
+
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// GLOBAL STATE
|
|
7
|
+
// ============================================================================
|
|
8
|
+
|
|
9
|
+
var cy = null; // Cytoscape.js instance (global)
|
|
10
|
+
var graphData = { nodes: [], links: [] }; // Raw data from API
|
|
11
|
+
var originalGraphData = { nodes: [], links: [] }; // Unfiltered data (for reset)
|
|
12
|
+
var currentLayout = 'fcose'; // Default layout
|
|
13
|
+
var filterState = { cluster_id: null, entity: null }; // Current filters
|
|
14
|
+
var isInitialLoad = true; // Track if this is the first graph load
|
|
15
|
+
var focusedNodeIndex = 0; // Keyboard navigation: currently focused node
|
|
16
|
+
var keyboardNavigationEnabled = false; // Track if keyboard nav is active
|
|
17
|
+
var lastFocusedElement = null; // Store last focused element for modal return
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// CLUSTER COLORS
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
const CLUSTER_COLORS = [
|
|
24
|
+
'#667eea', '#764ba2', '#43e97b', '#38f9d7',
|
|
25
|
+
'#4facfe', '#00f2fe', '#f093fb', '#f5576c',
|
|
26
|
+
'#fa709a', '#fee140', '#30cfd0', '#330867'
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function getClusterColor(cluster_id) {
|
|
30
|
+
if (!cluster_id) return '#999';
|
|
31
|
+
return CLUSTER_COLORS[cluster_id % CLUSTER_COLORS.length];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// HTML ESCAPE UTILITY
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
function escapeHtml(text) {
|
|
39
|
+
const div = document.createElement('div');
|
|
40
|
+
div.textContent = text;
|
|
41
|
+
return div.innerHTML;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// GRAPH LOADING
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
async function loadGraph() {
|
|
49
|
+
var maxNodes = document.getElementById('graph-max-nodes').value;
|
|
50
|
+
var minImportance = document.getElementById('graph-min-importance')?.value || 1;
|
|
51
|
+
|
|
52
|
+
// Get cluster filter from URL params ONLY on initial load
|
|
53
|
+
// After that, use filterState (which tab event handler controls)
|
|
54
|
+
if (isInitialLoad) {
|
|
55
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
56
|
+
const clusterIdParam = urlParams.get('cluster_id');
|
|
57
|
+
if (clusterIdParam) {
|
|
58
|
+
filterState.cluster_id = parseInt(clusterIdParam);
|
|
59
|
+
console.log('[loadGraph] Initial load with cluster filter from URL:', filterState.cluster_id);
|
|
60
|
+
}
|
|
61
|
+
isInitialLoad = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// CRITICAL FIX: When filtering by cluster, fetch MORE nodes to ensure all cluster members are included
|
|
65
|
+
// Otherwise only top N memories are fetched and cluster filter fails
|
|
66
|
+
const fetchLimit = filterState.cluster_id ? 200 : maxNodes;
|
|
67
|
+
console.log('[loadGraph] Fetching with limit:', fetchLimit, 'Cluster filter:', filterState.cluster_id);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
showLoadingSpinner();
|
|
71
|
+
const response = await fetch(`/api/graph?max_nodes=${fetchLimit}&min_importance=${minImportance}`);
|
|
72
|
+
graphData = await response.json();
|
|
73
|
+
originalGraphData = JSON.parse(JSON.stringify(graphData)); // Deep copy
|
|
74
|
+
|
|
75
|
+
// Apply filters if set
|
|
76
|
+
if (filterState.cluster_id) {
|
|
77
|
+
graphData = filterByCluster(originalGraphData, filterState.cluster_id);
|
|
78
|
+
}
|
|
79
|
+
if (filterState.entity) {
|
|
80
|
+
graphData = filterByEntity(originalGraphData, filterState.entity);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
renderGraph(graphData);
|
|
84
|
+
hideLoadingSpinner();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('Error loading graph:', error);
|
|
87
|
+
showError('Failed to load graph. Please try again.');
|
|
88
|
+
hideLoadingSpinner();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// DATA TRANSFORMATION
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
function transformDataForCytoscape(data) {
|
|
97
|
+
const elements = [];
|
|
98
|
+
|
|
99
|
+
// Add nodes
|
|
100
|
+
data.nodes.forEach(node => {
|
|
101
|
+
const label = node.category || node.project_name || `Memory #${node.id}`;
|
|
102
|
+
const contentPreview = node.content_preview || node.summary || node.content || '';
|
|
103
|
+
const preview = contentPreview.substring(0, 50) + (contentPreview.length > 50 ? '...' : '');
|
|
104
|
+
|
|
105
|
+
elements.push({
|
|
106
|
+
group: 'nodes',
|
|
107
|
+
data: {
|
|
108
|
+
id: String(node.id),
|
|
109
|
+
label: label,
|
|
110
|
+
content: node.content || '',
|
|
111
|
+
summary: node.summary || '',
|
|
112
|
+
content_preview: preview,
|
|
113
|
+
category: node.category || '',
|
|
114
|
+
project_name: node.project_name || '',
|
|
115
|
+
cluster_id: node.cluster_id || 0,
|
|
116
|
+
importance: node.importance || 5,
|
|
117
|
+
tags: node.tags || '',
|
|
118
|
+
entities: node.entities || [],
|
|
119
|
+
created_at: node.created_at || '',
|
|
120
|
+
// For rendering
|
|
121
|
+
weight: (node.importance || 5) * 5 // Size multiplier
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Add edges
|
|
127
|
+
data.links.forEach(link => {
|
|
128
|
+
const sourceId = String(typeof link.source === 'object' ? link.source.id : link.source);
|
|
129
|
+
const targetId = String(typeof link.target === 'object' ? link.target.id : link.target);
|
|
130
|
+
|
|
131
|
+
elements.push({
|
|
132
|
+
group: 'edges',
|
|
133
|
+
data: {
|
|
134
|
+
id: `${sourceId}-${targetId}`,
|
|
135
|
+
source: sourceId,
|
|
136
|
+
target: targetId,
|
|
137
|
+
weight: link.weight || 0.5,
|
|
138
|
+
relationship_type: link.relationship_type || '',
|
|
139
|
+
shared_entities: link.shared_entities || []
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
return elements;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ============================================================================
|
|
148
|
+
// GRAPH RENDERING
|
|
149
|
+
// ============================================================================
|
|
150
|
+
|
|
151
|
+
function renderGraph(data) {
|
|
152
|
+
const container = document.getElementById('graph-container');
|
|
153
|
+
if (!container) {
|
|
154
|
+
console.error('Graph container not found');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Clear existing graph
|
|
159
|
+
if (cy) {
|
|
160
|
+
cy.destroy();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check node count for performance tier
|
|
164
|
+
const nodeCount = data.nodes.length;
|
|
165
|
+
let layout = determineBestLayout(nodeCount);
|
|
166
|
+
|
|
167
|
+
// Transform data
|
|
168
|
+
const elements = transformDataForCytoscape(data);
|
|
169
|
+
|
|
170
|
+
if (elements.length === 0) {
|
|
171
|
+
const emptyMsg = document.createElement('div');
|
|
172
|
+
emptyMsg.style.cssText = 'text-align:center; padding:50px; color:#666;';
|
|
173
|
+
emptyMsg.textContent = 'No memories found. Try adjusting filters.';
|
|
174
|
+
container.textContent = ''; // Clear safely
|
|
175
|
+
container.appendChild(emptyMsg);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Clear container (remove spinner/old content) - safe approach
|
|
180
|
+
container.textContent = '';
|
|
181
|
+
console.log('[renderGraph] Container cleared, initializing Cytoscape with', elements.length, 'elements');
|
|
182
|
+
|
|
183
|
+
// Initialize Cytoscape WITHOUT running layout yet
|
|
184
|
+
try {
|
|
185
|
+
cy = cytoscape({
|
|
186
|
+
container: container,
|
|
187
|
+
elements: elements,
|
|
188
|
+
style: getCytoscapeStyles(),
|
|
189
|
+
layout: { name: 'preset' }, // Don't run layout yet
|
|
190
|
+
minZoom: 0.2,
|
|
191
|
+
maxZoom: 3,
|
|
192
|
+
wheelSensitivity: 0.05, // Reduced from 0.2 - less sensitive zoom
|
|
193
|
+
autoungrabify: false,
|
|
194
|
+
autounselectify: false,
|
|
195
|
+
// Mobile touch support
|
|
196
|
+
touchTapThreshold: 8, // Pixels of movement allowed for tap (vs drag)
|
|
197
|
+
desktopTapThreshold: 4, // Desktop click threshold
|
|
198
|
+
pixelRatio: 'auto' // Retina/HiDPI support
|
|
199
|
+
});
|
|
200
|
+
console.log('[renderGraph] Cytoscape initialized successfully, nodes:', cy.nodes().length);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
console.error('[renderGraph] Cytoscape initialization failed:', error);
|
|
203
|
+
showError('Failed to render graph: ' + error.message);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Add interactions
|
|
208
|
+
addCytoscapeInteractions();
|
|
209
|
+
|
|
210
|
+
// Add navigator (mini-map) if available
|
|
211
|
+
if (cy.navigator && nodeCount > 50) {
|
|
212
|
+
cy.navigator({
|
|
213
|
+
container: false, // Will be added to DOM separately if needed
|
|
214
|
+
viewLiveFramerate: 0,
|
|
215
|
+
dblClickDelay: 200,
|
|
216
|
+
removeCustomContainer: false,
|
|
217
|
+
rerenderDelay: 100
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Update UI
|
|
222
|
+
updateFilterBadge();
|
|
223
|
+
updateGraphStats(data);
|
|
224
|
+
|
|
225
|
+
// Announce graph load to screen readers
|
|
226
|
+
updateScreenReaderStatus(`Graph loaded with ${data.nodes.length} memories and ${data.links.length} connections`);
|
|
227
|
+
|
|
228
|
+
// Check if we should restore saved layout or run fresh layout
|
|
229
|
+
const hasSavedLayout = localStorage.getItem('slm_graph_layout');
|
|
230
|
+
|
|
231
|
+
if (hasSavedLayout) {
|
|
232
|
+
// Restore saved positions, then fit
|
|
233
|
+
restoreSavedLayout();
|
|
234
|
+
console.log('[renderGraph] Restored saved layout positions');
|
|
235
|
+
cy.fit(null, 80);
|
|
236
|
+
} else {
|
|
237
|
+
// No saved layout - run layout algorithm with fit
|
|
238
|
+
console.log('[renderGraph] Running fresh layout:', layout);
|
|
239
|
+
const layoutConfig = getLayoutConfig(layout);
|
|
240
|
+
const graphLayout = cy.layout(layoutConfig);
|
|
241
|
+
|
|
242
|
+
// CRITICAL: Wait for layout to finish, THEN force fit
|
|
243
|
+
graphLayout.on('layoutstop', function() {
|
|
244
|
+
console.log('[renderGraph] Layout completed, forcing fit to viewport');
|
|
245
|
+
cy.fit(null, 80); // Force center with 80px padding
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
graphLayout.run();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ============================================================================
|
|
253
|
+
// LAYOUT ALGORITHMS
|
|
254
|
+
// ============================================================================
|
|
255
|
+
|
|
256
|
+
function determineBestLayout(nodeCount) {
|
|
257
|
+
if (nodeCount <= 500) {
|
|
258
|
+
return 'fcose'; // Full interactive graph
|
|
259
|
+
} else if (nodeCount <= 2000) {
|
|
260
|
+
return 'cose'; // Faster force-directed
|
|
261
|
+
} else {
|
|
262
|
+
return 'circle'; // Focus mode (circular for large graphs)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function getLayoutConfig(layoutName) {
|
|
267
|
+
const configs = {
|
|
268
|
+
'fcose': {
|
|
269
|
+
name: 'fcose',
|
|
270
|
+
quality: 'default',
|
|
271
|
+
randomize: false,
|
|
272
|
+
animate: false, // Disabled for stability
|
|
273
|
+
fit: true,
|
|
274
|
+
padding: 80, // Increased padding to keep within bounds
|
|
275
|
+
nodeSeparation: 100,
|
|
276
|
+
idealEdgeLength: 100,
|
|
277
|
+
edgeElasticity: 0.45,
|
|
278
|
+
nestingFactor: 0.1,
|
|
279
|
+
gravity: 0.25,
|
|
280
|
+
numIter: 2500,
|
|
281
|
+
tile: true,
|
|
282
|
+
tilingPaddingVertical: 10,
|
|
283
|
+
tilingPaddingHorizontal: 10,
|
|
284
|
+
gravityRangeCompound: 1.5,
|
|
285
|
+
gravityCompound: 1.0,
|
|
286
|
+
gravityRange: 3.8
|
|
287
|
+
},
|
|
288
|
+
'cose': {
|
|
289
|
+
name: 'cose',
|
|
290
|
+
animate: false, // Disabled for stability
|
|
291
|
+
fit: true,
|
|
292
|
+
padding: 80, // Increased padding
|
|
293
|
+
nodeRepulsion: 8000,
|
|
294
|
+
idealEdgeLength: 100,
|
|
295
|
+
edgeElasticity: 100,
|
|
296
|
+
nestingFactor: 5,
|
|
297
|
+
gravity: 80,
|
|
298
|
+
numIter: 1000,
|
|
299
|
+
randomize: false
|
|
300
|
+
},
|
|
301
|
+
'circle': {
|
|
302
|
+
name: 'circle',
|
|
303
|
+
animate: true,
|
|
304
|
+
animationDuration: 1000,
|
|
305
|
+
fit: true,
|
|
306
|
+
padding: 50,
|
|
307
|
+
sort: function(a, b) {
|
|
308
|
+
return b.data('importance') - a.data('importance');
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
'grid': {
|
|
312
|
+
name: 'grid',
|
|
313
|
+
animate: true,
|
|
314
|
+
animationDuration: 1000,
|
|
315
|
+
fit: true,
|
|
316
|
+
padding: 50,
|
|
317
|
+
sort: function(a, b) {
|
|
318
|
+
return b.data('importance') - a.data('importance');
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
'breadthfirst': {
|
|
322
|
+
name: 'breadthfirst',
|
|
323
|
+
animate: true,
|
|
324
|
+
animationDuration: 1000,
|
|
325
|
+
fit: true,
|
|
326
|
+
padding: 50,
|
|
327
|
+
directed: false,
|
|
328
|
+
circle: false,
|
|
329
|
+
spacingFactor: 1.5,
|
|
330
|
+
sort: function(a, b) {
|
|
331
|
+
return b.data('importance') - a.data('importance');
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
'concentric': {
|
|
335
|
+
name: 'concentric',
|
|
336
|
+
animate: true,
|
|
337
|
+
animationDuration: 1000,
|
|
338
|
+
fit: true,
|
|
339
|
+
padding: 50,
|
|
340
|
+
concentric: function(node) {
|
|
341
|
+
return node.data('importance');
|
|
342
|
+
},
|
|
343
|
+
levelWidth: function() {
|
|
344
|
+
return 2;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
return configs[layoutName] || configs['fcose'];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ============================================================================
|
|
353
|
+
// CYTOSCAPE STYLES
|
|
354
|
+
// ============================================================================
|
|
355
|
+
|
|
356
|
+
function getCytoscapeStyles() {
|
|
357
|
+
return [
|
|
358
|
+
{
|
|
359
|
+
selector: 'node',
|
|
360
|
+
style: {
|
|
361
|
+
'background-color': function(ele) {
|
|
362
|
+
return getClusterColor(ele.data('cluster_id'));
|
|
363
|
+
},
|
|
364
|
+
'width': function(ele) {
|
|
365
|
+
return Math.max(20, ele.data('weight'));
|
|
366
|
+
},
|
|
367
|
+
'height': function(ele) {
|
|
368
|
+
return Math.max(20, ele.data('weight'));
|
|
369
|
+
},
|
|
370
|
+
'label': 'data(label)',
|
|
371
|
+
'font-size': '10px',
|
|
372
|
+
'text-valign': 'center',
|
|
373
|
+
'text-halign': 'center',
|
|
374
|
+
'color': '#333',
|
|
375
|
+
'text-outline-width': 2,
|
|
376
|
+
'text-outline-color': '#fff',
|
|
377
|
+
'border-width': function(ele) {
|
|
378
|
+
// Trust score → border thickness (if available)
|
|
379
|
+
return 2;
|
|
380
|
+
},
|
|
381
|
+
'border-color': '#555'
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
selector: 'node:selected',
|
|
386
|
+
style: {
|
|
387
|
+
'border-width': 4,
|
|
388
|
+
'border-color': '#667eea',
|
|
389
|
+
'background-color': '#667eea'
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
selector: 'node.highlighted',
|
|
394
|
+
style: {
|
|
395
|
+
'border-width': 4,
|
|
396
|
+
'border-color': '#ff6b6b',
|
|
397
|
+
'box-shadow': '0 0 20px #ff6b6b'
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
selector: 'node.dimmed',
|
|
402
|
+
style: {
|
|
403
|
+
'opacity': 0.3
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
selector: 'node.keyboard-focused',
|
|
408
|
+
style: {
|
|
409
|
+
'border-width': 5,
|
|
410
|
+
'border-color': '#0066ff',
|
|
411
|
+
'border-style': 'solid',
|
|
412
|
+
'box-shadow': '0 0 15px #0066ff'
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
selector: 'edge',
|
|
417
|
+
style: {
|
|
418
|
+
'width': function(ele) {
|
|
419
|
+
return Math.max(1, ele.data('weight') * 3);
|
|
420
|
+
},
|
|
421
|
+
'line-color': '#ccc',
|
|
422
|
+
'line-style': function(ele) {
|
|
423
|
+
return ele.data('weight') > 0.3 ? 'solid' : 'dashed';
|
|
424
|
+
},
|
|
425
|
+
'curve-style': 'bezier',
|
|
426
|
+
'target-arrow-shape': 'none',
|
|
427
|
+
'opacity': 0.6
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
selector: 'edge.highlighted',
|
|
432
|
+
style: {
|
|
433
|
+
'line-color': '#667eea',
|
|
434
|
+
'width': 3,
|
|
435
|
+
'opacity': 1
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
selector: 'edge.dimmed',
|
|
440
|
+
style: {
|
|
441
|
+
'opacity': 0.1
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
];
|
|
445
|
+
}
|