912 lines
29 KiB
JavaScript
912 lines
29 KiB
JavaScript
/**
|
|
* Graph visualization module for DNSRecon
|
|
* Handles network graph rendering using vis.js with enhanced Phase 2 features
|
|
*/
|
|
|
|
class GraphManager {
|
|
constructor(containerId) {
|
|
this.container = document.getElementById(containerId);
|
|
this.network = null;
|
|
this.nodes = new vis.DataSet();
|
|
this.edges = new vis.DataSet();
|
|
this.isInitialized = false;
|
|
this.currentLayout = 'physics';
|
|
this.nodeInfoPopup = null;
|
|
|
|
this.options = {
|
|
nodes: {
|
|
shape: 'dot',
|
|
size: 15,
|
|
font: {
|
|
size: 12,
|
|
color: '#c7c7c7',
|
|
face: 'Roboto Mono, monospace',
|
|
background: 'rgba(26, 26, 26, 0.9)',
|
|
strokeWidth: 2,
|
|
strokeColor: '#000000'
|
|
},
|
|
borderWidth: 2,
|
|
borderColor: '#444',
|
|
scaling: {
|
|
min: 10,
|
|
max: 30,
|
|
label: {
|
|
enabled: true,
|
|
min: 8,
|
|
max: 16
|
|
}
|
|
},
|
|
chosen: {
|
|
node: (values, id, selected, hovering) => {
|
|
values.borderColor = '#00ff41';
|
|
values.borderWidth = 3;
|
|
}
|
|
}
|
|
},
|
|
edges: {
|
|
width: 2,
|
|
color: {
|
|
color: '#555',
|
|
highlight: '#00ff41',
|
|
hover: '#ff9900',
|
|
inherit: false
|
|
},
|
|
font: {
|
|
size: 10,
|
|
color: '#999',
|
|
face: 'Roboto Mono, monospace',
|
|
background: 'rgba(26, 26, 26, 0.8)',
|
|
strokeWidth: 1,
|
|
strokeColor: '#000000'
|
|
},
|
|
arrows: {
|
|
to: {
|
|
enabled: true,
|
|
scaleFactor: 1,
|
|
type: 'arrow'
|
|
}
|
|
},
|
|
smooth: {
|
|
enabled: true,
|
|
type: 'dynamic',
|
|
roundness: 0.6
|
|
},
|
|
chosen: {
|
|
edge: (values, id, selected, hovering) => {
|
|
values.color = '#00ff41';
|
|
values.width = 4;
|
|
}
|
|
}
|
|
},
|
|
physics: {
|
|
enabled: true,
|
|
stabilization: {
|
|
enabled: true,
|
|
iterations: 150,
|
|
updateInterval: 50
|
|
},
|
|
barnesHut: {
|
|
gravitationalConstant: -3000,
|
|
centralGravity: 0.4,
|
|
springLength: 120,
|
|
springConstant: 0.05,
|
|
damping: 0.1,
|
|
avoidOverlap: 0.2
|
|
},
|
|
maxVelocity: 30,
|
|
minVelocity: 0.1,
|
|
solver: 'barnesHut',
|
|
timestep: 0.4,
|
|
adaptiveTimestep: true
|
|
},
|
|
interaction: {
|
|
hover: true,
|
|
hoverConnectedEdges: true,
|
|
selectConnectedEdges: true,
|
|
tooltipDelay: 300,
|
|
hideEdgesOnDrag: false,
|
|
hideNodesOnDrag: false,
|
|
zoomView: true,
|
|
dragView: true,
|
|
multiselect: true
|
|
},
|
|
layout: {
|
|
improvedLayout: true,
|
|
randomSeed: 2
|
|
}
|
|
};
|
|
|
|
this.createNodeInfoPopup();
|
|
}
|
|
|
|
/**
|
|
* Create floating node info popup
|
|
*/
|
|
createNodeInfoPopup() {
|
|
this.nodeInfoPopup = document.createElement('div');
|
|
this.nodeInfoPopup.className = 'node-info-popup';
|
|
this.nodeInfoPopup.style.display = 'none';
|
|
document.body.appendChild(this.nodeInfoPopup);
|
|
}
|
|
|
|
/**
|
|
* Initialize the network graph with enhanced features
|
|
*/
|
|
initialize() {
|
|
if (this.isInitialized) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const data = {
|
|
nodes: this.nodes,
|
|
edges: this.edges
|
|
};
|
|
|
|
this.network = new vis.Network(this.container, data, this.options);
|
|
this.setupNetworkEvents();
|
|
this.isInitialized = true;
|
|
|
|
// Hide placeholder
|
|
const placeholder = this.container.querySelector('.graph-placeholder');
|
|
if (placeholder) {
|
|
placeholder.style.display = 'none';
|
|
}
|
|
|
|
// Add graph controls
|
|
this.addGraphControls();
|
|
|
|
console.log('Enhanced graph initialized successfully');
|
|
} catch (error) {
|
|
console.error('Failed to initialize graph:', error);
|
|
this.showError('Failed to initialize visualization');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add interactive graph controls
|
|
*/
|
|
addGraphControls() {
|
|
const controlsContainer = document.createElement('div');
|
|
controlsContainer.className = 'graph-controls';
|
|
controlsContainer.innerHTML = `
|
|
<button class="graph-control-btn" id="graph-fit" title="Fit to Screen">[FIT]</button>
|
|
<button class="graph-control-btn" id="graph-physics" title="Toggle Physics">[PHYSICS]</button>
|
|
<button class="graph-control-btn" id="graph-cluster" title="Cluster Nodes">[CLUSTER]</button>
|
|
`;
|
|
|
|
this.container.appendChild(controlsContainer);
|
|
|
|
// Add control event listeners
|
|
document.getElementById('graph-fit').addEventListener('click', () => this.fitView());
|
|
document.getElementById('graph-physics').addEventListener('click', () => this.togglePhysics());
|
|
document.getElementById('graph-cluster').addEventListener('click', () => this.toggleClustering());
|
|
}
|
|
|
|
/**
|
|
* Setup enhanced network event handlers
|
|
*/
|
|
setupNetworkEvents() {
|
|
if (!this.network) return;
|
|
|
|
// Node click event with enhanced details
|
|
this.network.on('click', (params) => {
|
|
if (params.nodes.length > 0) {
|
|
const nodeId = params.nodes[0];
|
|
if (this.network.isCluster(nodeId)) {
|
|
this.network.openCluster(nodeId);
|
|
} else {
|
|
this.showNodeDetails(nodeId);
|
|
this.highlightNodeConnections(nodeId);
|
|
}
|
|
} else {
|
|
this.clearHighlights();
|
|
}
|
|
});
|
|
|
|
// Enhanced hover events
|
|
this.network.on('hoverNode', (params) => {
|
|
const nodeId = params.node;
|
|
const node = this.nodes.get(nodeId);
|
|
if (node) {
|
|
this.highlightConnectedNodes(nodeId, true);
|
|
}
|
|
});
|
|
|
|
// FIX: Comment out the problematic context menu handler
|
|
this.network.on('oncontext', (params) => {
|
|
params.event.preventDefault();
|
|
// if (params.nodes.length > 0) {
|
|
// this.showNodeContextMenu(params.pointer.DOM, params.nodes[0]);
|
|
// }
|
|
});
|
|
|
|
// Stabilization events with progress
|
|
this.network.on('stabilizationProgress', (params) => {
|
|
const progress = params.iterations / params.total;
|
|
this.updateStabilizationProgress(progress);
|
|
});
|
|
|
|
this.network.on('stabilizationIterationsDone', () => {
|
|
this.onStabilizationComplete();
|
|
});
|
|
|
|
// Selection events
|
|
this.network.on('select', (params) => {
|
|
console.log('Selected nodes:', params.nodes);
|
|
console.log('Selected edges:', params.edges);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Update graph with new data and enhanced processing
|
|
* @param {Object} graphData - Graph data from backend
|
|
*/
|
|
updateGraph(graphData) {
|
|
if (!graphData || !graphData.nodes || !graphData.edges) {
|
|
console.warn('Invalid graph data received');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// Initialize if not already done
|
|
if (!this.isInitialized) {
|
|
this.initialize();
|
|
}
|
|
|
|
const largeEntityMap = new Map();
|
|
graphData.nodes.forEach(node => {
|
|
if (node.type === 'large_entity' && node.attributes && Array.isArray(node.attributes.nodes)) {
|
|
node.attributes.nodes.forEach(nodeId => {
|
|
largeEntityMap.set(nodeId, node.id);
|
|
});
|
|
}
|
|
});
|
|
|
|
const processedNodes = graphData.nodes.map(node => {
|
|
const processed = this.processNode(node);
|
|
if (largeEntityMap.has(node.id)) {
|
|
processed.hidden = true;
|
|
}
|
|
return processed;
|
|
});
|
|
|
|
const mergedEdges = {};
|
|
graphData.edges.forEach(edge => {
|
|
const fromNode = largeEntityMap.has(edge.from) ? largeEntityMap.get(edge.from) : edge.from;
|
|
const toNode = largeEntityMap.has(edge.to) ? largeEntityMap.get(edge.to) : edge.to;
|
|
const mergeKey = `${fromNode}-${toNode}-${edge.label}`;
|
|
|
|
if (!mergedEdges[mergeKey]) {
|
|
mergedEdges[mergeKey] = {
|
|
...edge,
|
|
from: fromNode,
|
|
to: toNode,
|
|
count: 0,
|
|
confidence_score: 0
|
|
};
|
|
}
|
|
|
|
mergedEdges[mergeKey].count++;
|
|
if (edge.confidence_score > mergedEdges[mergeKey].confidence_score) {
|
|
mergedEdges[mergeKey].confidence_score = edge.confidence_score;
|
|
}
|
|
});
|
|
|
|
const processedEdges = Object.values(mergedEdges).map(edge => {
|
|
const processed = this.processEdge(edge);
|
|
if (edge.count > 1) {
|
|
processed.label = `${edge.label} (${edge.count})`;
|
|
}
|
|
return processed;
|
|
});
|
|
|
|
// Update datasets with animation
|
|
const existingNodeIds = this.nodes.getIds();
|
|
const existingEdgeIds = this.edges.getIds();
|
|
|
|
// Add new nodes with fade-in animation
|
|
const newNodes = processedNodes.filter(node => !existingNodeIds.includes(node.id));
|
|
const newEdges = processedEdges.filter(edge => !existingEdgeIds.includes(edge.id));
|
|
|
|
// Update existing data
|
|
this.nodes.update(processedNodes);
|
|
this.edges.update(processedEdges);
|
|
|
|
// Highlight new additions briefly
|
|
if (newNodes.length > 0 || newEdges.length > 0) {
|
|
setTimeout(() => this.highlightNewElements(newNodes, newEdges), 100);
|
|
}
|
|
|
|
// Auto-fit view for small graphs or first update
|
|
if (processedNodes.length <= 10 || existingNodeIds.length === 0) {
|
|
setTimeout(() => this.fitView(), 800);
|
|
}
|
|
|
|
console.log(`Enhanced graph updated: ${processedNodes.length} nodes, ${processedEdges.length} edges (${newNodes.length} new nodes, ${newEdges.length} new edges)`);
|
|
} catch (error) {
|
|
console.error('Failed to update enhanced graph:', error);
|
|
this.showError('Failed to update visualization');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process node data with enhanced styling and metadata
|
|
* @param {Object} node - Raw node data
|
|
* @returns {Object} Processed node data
|
|
*/
|
|
processNode(node) {
|
|
const processedNode = {
|
|
id: node.id,
|
|
label: this.formatNodeLabel(node.id, node.type),
|
|
color: this.getNodeColor(node.type),
|
|
size: this.getNodeSize(node.type),
|
|
borderColor: this.getNodeBorderColor(node.type),
|
|
shape: this.getNodeShape(node.type),
|
|
attributes: node.attributes || {},
|
|
description: node.description || '',
|
|
metadata: node.metadata || {},
|
|
type: node.type
|
|
};
|
|
|
|
// Add confidence-based styling
|
|
if (node.confidence) {
|
|
processedNode.borderWidth = Math.max(2, Math.floor(node.confidence * 5));
|
|
}
|
|
|
|
// Style based on certificate validity
|
|
if (node.type === 'domain') {
|
|
if (node.attributes && node.attributes.certificates && node.attributes.certificates.has_valid_cert === false) {
|
|
processedNode.color = { background: '#888888', border: '#666666' };
|
|
}
|
|
}
|
|
|
|
if (node.type === 'correlation_object') {
|
|
processedNode.label = this.formatNodeLabel(node.metadata.value, node.type);
|
|
}
|
|
|
|
return processedNode;
|
|
}
|
|
|
|
/**
|
|
* Process edge data with enhanced styling and metadata
|
|
* @param {Object} edge - Raw edge data
|
|
* @returns {Object} Processed edge data
|
|
*/
|
|
processEdge(edge) {
|
|
const confidence = edge.confidence_score || 0;
|
|
const processedEdge = {
|
|
id: `${edge.from}-${edge.to}`,
|
|
from: edge.from,
|
|
to: edge.to,
|
|
label: this.formatEdgeLabel(edge.label, confidence),
|
|
title: this.createEdgeTooltip(edge),
|
|
width: this.getEdgeWidth(confidence),
|
|
color: this.getEdgeColor(confidence),
|
|
dashes: confidence < 0.6 ? [5, 5] : false,
|
|
metadata: {
|
|
relationship_type: edge.label,
|
|
confidence_score: confidence,
|
|
source_provider: edge.source_provider,
|
|
discovery_timestamp: edge.discovery_timestamp
|
|
}
|
|
};
|
|
|
|
|
|
|
|
return processedEdge;
|
|
}
|
|
|
|
/**
|
|
* Format node label for display
|
|
* @param {string} nodeId - Node identifier
|
|
* @param {string} nodeType - Node type
|
|
* @returns {string} Formatted label
|
|
*/
|
|
formatNodeLabel(nodeId, nodeType) {
|
|
if (typeof nodeId !== 'string') return '';
|
|
if (nodeId.length > 20) {
|
|
return nodeId.substring(0, 17) + '...';
|
|
}
|
|
return nodeId;
|
|
}
|
|
|
|
/**
|
|
* Format edge label for display
|
|
* @param {string} relationshipType - Type of relationship
|
|
* @param {number} confidence - Confidence score
|
|
* @returns {string} Formatted label
|
|
*/
|
|
formatEdgeLabel(relationshipType, confidence) {
|
|
if (!relationshipType) return '';
|
|
|
|
const confidenceText = confidence >= 0.8 ? '●' : confidence >= 0.6 ? '◐' : '○';
|
|
return `${relationshipType} ${confidenceText}`;
|
|
}
|
|
|
|
/**
|
|
* Get node color based on type
|
|
* @param {string} nodeType - Node type
|
|
* @returns {string} Color value
|
|
*/
|
|
getNodeColor(nodeType) {
|
|
const colors = {
|
|
'domain': '#00ff41', // Green
|
|
'ip': '#ff9900', // Amber
|
|
'asn': '#00aaff', // Blue
|
|
'large_entity': '#ff6b6b', // Red for large entities
|
|
'correlation_object': '#9620c0ff'
|
|
};
|
|
return colors[nodeType] || '#ffffff';
|
|
}
|
|
|
|
|
|
/**
|
|
* Get node border color based on type
|
|
* @param {string} nodeType - Node type
|
|
* @returns {string} Border color value
|
|
*/
|
|
getNodeBorderColor(nodeType) {
|
|
const borderColors = {
|
|
'domain': '#00aa2e',
|
|
'ip': '#cc7700',
|
|
'asn': '#0088cc',
|
|
'correlation_object': '#c235c9ff'
|
|
};
|
|
return borderColors[nodeType] || '#666666';
|
|
}
|
|
|
|
/**
|
|
* Get node size based on type
|
|
* @param {string} nodeType - Node type
|
|
* @returns {number} Node size
|
|
*/
|
|
getNodeSize(nodeType) {
|
|
const sizes = {
|
|
'domain': 12,
|
|
'ip': 14,
|
|
'asn': 16,
|
|
'correlation_object': 8,
|
|
'large_entity': 5
|
|
};
|
|
return sizes[nodeType] || 12;
|
|
}
|
|
|
|
/**
|
|
* Get enhanced node shape based on type
|
|
* @param {string} nodeType - Node type
|
|
* @returns {string} Shape name
|
|
*/
|
|
getNodeShape(nodeType) {
|
|
const shapes = {
|
|
'domain': 'dot',
|
|
'ip': 'square',
|
|
'asn': 'triangle',
|
|
'correlation_object': 'hexagon',
|
|
'large_entity': 'database'
|
|
};
|
|
return shapes[nodeType] || 'dot';
|
|
}
|
|
|
|
/**
|
|
* Get edge color based on confidence
|
|
* @param {number} confidence - Confidence score
|
|
* @returns {string} Edge color
|
|
*/
|
|
getEdgeColor(confidence) {
|
|
if (confidence >= 0.8) {
|
|
return '#00ff41'; // High confidence - green
|
|
} else if (confidence >= 0.6) {
|
|
return '#ff9900'; // Medium confidence - amber
|
|
} else {
|
|
return '#666666'; // Low confidence - gray
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get edge width based on confidence
|
|
* @param {number} confidence - Confidence score
|
|
* @returns {number} Edge width
|
|
*/
|
|
getEdgeWidth(confidence) {
|
|
if (confidence >= 0.8) {
|
|
return 3;
|
|
} else if (confidence >= 0.6) {
|
|
return 2;
|
|
} else {
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create edge tooltip with correct provider information
|
|
* @param {Object} edge - Edge data
|
|
* @returns {string} HTML tooltip content
|
|
*/
|
|
createEdgeTooltip(edge) {
|
|
let tooltip = `<div style="font-family: 'Roboto Mono', monospace; font-size: 11px;">`;
|
|
tooltip += `<div style="color: #00ff41; font-weight: bold; margin-bottom: 4px;">${edge.label || 'Relationship'}</div>`;
|
|
tooltip += `<div style="color: #999; margin-bottom: 2px;">Confidence: ${(edge.confidence_score * 100).toFixed(1)}%</div>`;
|
|
|
|
if (edge.source_provider) {
|
|
tooltip += `<div style="color: #999; margin-bottom: 2px;">Provider: ${edge.source_provider}</div>`;
|
|
}
|
|
|
|
if (edge.discovery_timestamp) {
|
|
const date = new Date(edge.discovery_timestamp);
|
|
tooltip += `<div style="color: #666; font-size: 10px;">Discovered: ${date.toLocaleString()}</div>`;
|
|
}
|
|
|
|
tooltip += `</div>`;
|
|
return tooltip;
|
|
}
|
|
|
|
/**
|
|
* Determine if node is important based on connections or metadata
|
|
* @param {Object} node - Node data
|
|
* @returns {boolean} True if node is important
|
|
*/
|
|
isImportantNode(node) {
|
|
// Mark nodes as important based on criteria
|
|
if (node.type === 'domain' && node.id.includes('www.')) return false;
|
|
if (node.metadata && node.metadata.connection_count > 3) return true;
|
|
if (node.type === 'asn') return true;
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Show node details in modal
|
|
* @param {string} nodeId - Node identifier
|
|
*/
|
|
showNodeDetails(nodeId) {
|
|
const node = this.nodes.get(nodeId);
|
|
if (!node) return;
|
|
|
|
// Trigger custom event for main application to handle
|
|
const event = new CustomEvent('nodeSelected', {
|
|
detail: { node }
|
|
});
|
|
document.dispatchEvent(event);
|
|
}
|
|
|
|
/**
|
|
* Hide node info popup
|
|
*/
|
|
hideNodeInfoPopup() {
|
|
if (this.nodeInfoPopup) {
|
|
this.nodeInfoPopup.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Highlight node connections
|
|
* @param {string} nodeId - Node to highlight
|
|
*/
|
|
highlightNodeConnections(nodeId) {
|
|
const connectedNodes = this.network.getConnectedNodes(nodeId);
|
|
const connectedEdges = this.network.getConnectedEdges(nodeId);
|
|
|
|
// Update node colors
|
|
const nodeUpdates = connectedNodes.map(id => ({
|
|
id: id,
|
|
borderColor: '#ff9900',
|
|
borderWidth: 3
|
|
}));
|
|
|
|
nodeUpdates.push({
|
|
id: nodeId,
|
|
borderColor: '#00ff41',
|
|
borderWidth: 4
|
|
});
|
|
|
|
// Update edge colors
|
|
const edgeUpdates = connectedEdges.map(id => ({
|
|
id: id,
|
|
color: { color: '#ff9900' },
|
|
width: 3
|
|
}));
|
|
|
|
this.nodes.update(nodeUpdates);
|
|
this.edges.update(edgeUpdates);
|
|
|
|
// Store for cleanup
|
|
this.highlightedElements = {
|
|
nodes: connectedNodes.concat([nodeId]),
|
|
edges: connectedEdges
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Highlight connected nodes on hover
|
|
* @param {string} nodeId - Node ID
|
|
* @param {boolean} highlight - Whether to highlight or unhighlight
|
|
*/
|
|
highlightConnectedNodes(nodeId, highlight) {
|
|
const connectedNodes = this.network.getConnectedNodes(nodeId);
|
|
const connectedEdges = this.network.getConnectedEdges(nodeId);
|
|
|
|
if (highlight) {
|
|
// Dim all other elements
|
|
this.dimUnconnectedElements([nodeId, ...connectedNodes], connectedEdges);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Dim elements not connected to the specified nodes
|
|
* @param {Array} nodeIds - Node IDs to keep highlighted
|
|
* @param {Array} edgeIds - Edge IDs to keep highlighted
|
|
*/
|
|
dimUnconnectedElements(nodeIds, edgeIds) {
|
|
const allNodes = this.nodes.get();
|
|
const allEdges = this.edges.get();
|
|
|
|
const nodeUpdates = allNodes.map(node => ({
|
|
id: node.id,
|
|
opacity: nodeIds.includes(node.id) ? 1 : 0.3
|
|
}));
|
|
|
|
const edgeUpdates = allEdges.map(edge => ({
|
|
id: edge.id,
|
|
opacity: edgeIds.includes(edge.id) ? 1 : 0.1
|
|
}));
|
|
|
|
this.nodes.update(nodeUpdates);
|
|
this.edges.update(edgeUpdates);
|
|
}
|
|
|
|
/**
|
|
* Clear all highlights
|
|
*/
|
|
clearHighlights() {
|
|
if (this.highlightedElements) {
|
|
// Reset highlighted nodes
|
|
const nodeUpdates = this.highlightedElements.nodes.map(id => {
|
|
const originalNode = this.nodes.get(id);
|
|
return {
|
|
id: id,
|
|
borderColor: this.getNodeBorderColor(originalNode.type),
|
|
borderWidth: 2
|
|
};
|
|
});
|
|
|
|
// Reset highlighted edges
|
|
const edgeUpdates = this.highlightedElements.edges.map(id => {
|
|
const originalEdge = this.edges.get(id);
|
|
return {
|
|
id: id,
|
|
color: this.getEdgeColor(originalEdge.metadata ? originalEdge.metadata.confidence_score : 0.5),
|
|
width: this.getEdgeWidth(originalEdge.metadata ? originalEdge.metadata.confidence_score : 0.5)
|
|
};
|
|
});
|
|
|
|
this.nodes.update(nodeUpdates);
|
|
this.edges.update(edgeUpdates);
|
|
|
|
this.highlightedElements = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear hover highlights
|
|
*/
|
|
clearHoverHighlights() {
|
|
const allNodes = this.nodes.get();
|
|
const allEdges = this.edges.get();
|
|
|
|
const nodeUpdates = allNodes.map(node => ({ id: node.id, opacity: 1 }));
|
|
const edgeUpdates = allEdges.map(edge => ({ id: edge.id, opacity: 1 }));
|
|
|
|
this.nodes.update(nodeUpdates);
|
|
this.edges.update(edgeUpdates);
|
|
}
|
|
|
|
/**
|
|
* Highlight newly added elements
|
|
* @param {Array} newNodes - New nodes
|
|
* @param {Array} newEdges - New edges
|
|
*/
|
|
highlightNewElements(newNodes, newEdges) {
|
|
// Briefly highlight new nodes
|
|
const nodeHighlights = newNodes.map(node => ({
|
|
id: node.id,
|
|
borderColor: '#00ff41',
|
|
borderWidth: 4
|
|
}));
|
|
|
|
// Briefly highlight new edges
|
|
const edgeHighlights = newEdges.map(edge => ({
|
|
id: edge.id,
|
|
color: '#00ff41',
|
|
width: 4
|
|
}));
|
|
|
|
this.nodes.update(nodeHighlights);
|
|
this.edges.update(edgeHighlights);
|
|
|
|
// Reset after animation
|
|
setTimeout(() => {
|
|
const nodeResets = newNodes.map(node => ({
|
|
id: node.id,
|
|
borderColor: this.getNodeBorderColor(node.type),
|
|
borderWidth: 2,
|
|
}));
|
|
|
|
const edgeResets = newEdges.map(edge => ({
|
|
id: edge.id,
|
|
color: this.getEdgeColor(edge.metadata ? edge.metadata.confidence_score : 0.5),
|
|
width: this.getEdgeWidth(edge.metadata ? edge.metadata.confidence_score : 0.5)
|
|
}));
|
|
|
|
this.nodes.update(nodeResets);
|
|
this.edges.update(edgeResets);
|
|
}, 2000);
|
|
}
|
|
|
|
/**
|
|
* Update stabilization progress
|
|
* @param {number} progress - Progress value (0-1)
|
|
*/
|
|
updateStabilizationProgress(progress) {
|
|
// Could show a progress indicator if needed
|
|
console.log(`Graph stabilization: ${(progress * 100).toFixed(1)}%`);
|
|
}
|
|
|
|
/**
|
|
* Handle stabilization completion
|
|
*/
|
|
onStabilizationComplete() {
|
|
console.log('Graph stabilization complete');
|
|
}
|
|
|
|
/**
|
|
* Focus view on specific node
|
|
* @param {string} nodeId - Node to focus on
|
|
*/
|
|
focusOnNode(nodeId) {
|
|
const nodePosition = this.network.getPositions([nodeId]);
|
|
if (nodePosition[nodeId]) {
|
|
this.network.moveTo({
|
|
position: nodePosition[nodeId],
|
|
scale: 1.5,
|
|
animation: {
|
|
duration: 1000,
|
|
easingFunction: 'easeInOutQuart'
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle physics simulation
|
|
*/
|
|
togglePhysics() {
|
|
const currentPhysics = this.network.physics.physicsEnabled;
|
|
this.network.setOptions({ physics: !currentPhysics });
|
|
|
|
const button = document.getElementById('graph-physics');
|
|
if (button) {
|
|
button.textContent = currentPhysics ? '[PHYSICS OFF]' : '[PHYSICS ON]';
|
|
button.style.color = currentPhysics ? '#ff9900' : '#00ff41';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Toggle node clustering
|
|
*/
|
|
toggleClustering() {
|
|
if (this.network.isCluster('domain-cluster')) {
|
|
this.network.openCluster('domain-cluster');
|
|
} else {
|
|
const clusterOptions = {
|
|
joinCondition: (nodeOptions) => {
|
|
return nodeOptions.type === 'domain';
|
|
},
|
|
clusterNodeProperties: {
|
|
id: 'domain-cluster',
|
|
label: 'Domains',
|
|
shape: 'database',
|
|
color: '#00ff41',
|
|
borderWidth: 3,
|
|
}
|
|
};
|
|
this.network.cluster(clusterOptions);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fit the view to show all nodes
|
|
*/
|
|
fitView() {
|
|
if (this.network) {
|
|
this.network.fit({
|
|
animation: {
|
|
duration: 1000,
|
|
easingFunction: 'easeInOutQuad'
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear the graph
|
|
*/
|
|
clear() {
|
|
this.nodes.clear();
|
|
this.edges.clear();
|
|
|
|
// Show placeholder
|
|
const placeholder = this.container.querySelector('.graph-placeholder');
|
|
if (placeholder) {
|
|
placeholder.style.display = 'flex';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show error message
|
|
* @param {string} message - Error message
|
|
*/
|
|
showError(message) {
|
|
const placeholder = this.container.querySelector('.graph-placeholder .placeholder-text');
|
|
if (placeholder) {
|
|
placeholder.textContent = `Error: ${message}`;
|
|
placeholder.style.color = '#ff6b6b';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get network statistics
|
|
* @returns {Object} Statistics object
|
|
*/
|
|
getStatistics() {
|
|
return {
|
|
nodeCount: this.nodes.length,
|
|
edgeCount: this.edges.length,
|
|
//isStabilized: this.network ? this.network.isStabilized() : false
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Apply filters to the graph
|
|
* @param {string} nodeType - The type of node to show ('all' for no filter)
|
|
* @param {number} minConfidence - The minimum confidence score for edges to be visible
|
|
*/
|
|
applyFilters(nodeType, minConfidence) {
|
|
console.log(`Applying filters: nodeType=${nodeType}, minConfidence=${minConfidence}`);
|
|
|
|
const nodeUpdates = [];
|
|
const edgeUpdates = [];
|
|
|
|
const allNodes = this.nodes.get({ returnType: 'Object' });
|
|
const allEdges = this.edges.get();
|
|
|
|
// Determine which nodes are visible based on the nodeType filter
|
|
for (const nodeId in allNodes) {
|
|
const node = allNodes[nodeId];
|
|
const isVisible = (nodeType === 'all' || node.type === nodeType);
|
|
nodeUpdates.push({ id: nodeId, hidden: !isVisible });
|
|
}
|
|
|
|
// Update nodes first to determine edge visibility
|
|
this.nodes.update(nodeUpdates);
|
|
|
|
// Determine which edges are visible based on confidence and connected nodes
|
|
for (const edge of allEdges) {
|
|
const sourceNode = this.nodes.get(edge.from);
|
|
const targetNode = this.nodes.get(edge.to);
|
|
const confidence = edge.metadata ? edge.metadata.confidence_score : 0;
|
|
|
|
const isVisible = confidence >= minConfidence &&
|
|
sourceNode && !sourceNode.hidden &&
|
|
targetNode && !targetNode.hidden;
|
|
|
|
edgeUpdates.push({ id: edge.id, hidden: !isVisible });
|
|
}
|
|
|
|
this.edges.update(edgeUpdates);
|
|
|
|
console.log('Filters applied.');
|
|
}
|
|
}
|
|
|
|
// Export for use in main.js
|
|
window.GraphManager = GraphManager; |