# Events Feature ## Overview The Events feature provides real-time visualization of WebSocket events streaming through the SPORE cluster. It displays events as an interactive force-directed graph, showing the flow and relationships between different event topics.  ## Features ### Real-Time Event Visualization - **Interactive Graph**: Events are visualized as a force-directed graph using D3.js - **Topic Chain Visualization**: Multi-part topics (e.g., `cluster/event/api/neopattern`) are broken down into chains showing hierarchical relationships - **Event Counting**: Each event type is tracked with occurrence counts displayed on connections - **Animated Transitions**: New events trigger visual animations showing data flow through the graph ### User Interactions #### Drag to Reposition Nodes - **Center Node**: Dragging the center (blue) node moves the entire graph together - **Topic Nodes**: Individual topic nodes (green) can be repositioned independently - **Fixed Positioning**: Once dragged, nodes maintain their positions to prevent layout disruption #### Zoom Controls - **Mouse Wheel**: Zoom in and out using the mouse wheel - **Scale Range**: Zoom level constrained between 0.5x and 5x - **Pan**: Click and drag the canvas to pan around the graph #### Rearrange Layout - **Rearrange Button**: Click the button in the top-left corner to reset node positions - **Automatic Layout**: Clears fixed positions and lets the force simulation reposition nodes ### Visual Elements #### Node Types - **Center Node** (Blue): - Represents the central event hub - Always visible in the graph - Acts as the root node for all event chains - Larger size (18px radius) - **Topic Nodes** (Green): - Represent individual topic segments - Examples: `cluster`, `event`, `api`, `neopattern` - Smaller size (14px radius) - Positioned dynamically based on event frequency #### Link Types - **Center Links** (Blue): - Connect the center node to root topic nodes - Thicker lines (2.5px) - Examples: center → `cluster`, center → `event` - **Topic Links** (Green): - Connect adjacent topics in event chains - Line thickness proportional to event frequency - Examples: `cluster` → `event`, `api` → `neopattern` #### Hover Interactions - **Link Hover**: Links become thicker and more opaque when hovered - **Event Frequency**: Line thickness dynamically adjusts based on event occurrence counts ## Architecture ### Component Structure ```12:37:spore-ui/public/scripts/components/EventComponent.js // Events Component - Visualizes websocket events as a graph class EventComponent extends Component { constructor(container, viewModel, eventBus) { super(container, viewModel, eventBus); this.svg = null; this.simulation = null; this.zoom = null; this.width = 0; this.height = 0; this.isInitialized = false; // Center node data - will be initialized with proper coordinates later this.centerNode = { id: 'center', type: 'center', label: '', x: 0, y: 0 }; // Track nodes for D3 this.graphNodes = []; this.graphLinks = []; // Track recent events to trigger animations this.lastSeenEvents = new Set(); } ``` ### ViewModel The `EventViewModel` manages the state and WebSocket connections: ```1241:1253:spore-ui/public/scripts/view-models.js // Events View Model for websocket event visualization class EventViewModel extends ViewModel { constructor() { super(); this.setMultiple({ events: new Map(), // Map of topic -> { parts: [], count: number, lastSeen: timestamp } isLoading: false, error: null, lastUpdateTime: null }); // Set up WebSocket listeners for real-time updates this.setupWebSocketListeners(); } ``` ### Event Tracking Events are tracked with the following metadata: - **Topic**: The full event topic path (e.g., `cluster/event/api/neopattern`) - **Parts**: Array of topic segments split by `/` - **Count**: Total occurrences of this event - **First Seen**: ISO timestamp of first occurrence - **Last Seen**: ISO timestamp of most recent occurrence - **Last Data**: The most recent event payload ### Event Addition Logic The view model handles nested events specially: ```1283:1329:spore-ui/public/scripts/view-models.js // Add a topic (parsed by "/" separator) addTopic(topic, data = null) { // Get current events as a new Map to ensure change detection const events = new Map(this.get('events')); // Handle nested events from cluster/event let fullTopic = topic; if (topic === 'cluster/event' && data && data.data) { try { const parsedData = typeof data.data === 'string' ? JSON.parse(data.data) : data.data; if (parsedData && parsedData.event) { // Create nested topic chain: cluster/event/api/neopattern fullTopic = `${topic}/${parsedData.event}`; } } catch (e) { // If parsing fails, just use the original topic } } const parts = fullTopic.split('/').filter(p => p); if (events.has(fullTopic)) { // Update existing event - create new object to ensure change detection const existing = events.get(fullTopic); events.set(fullTopic, { topic: existing.topic, parts: existing.parts, count: existing.count + 1, firstSeen: existing.firstSeen, lastSeen: new Date().toISOString(), lastData: data }); } else { // Create new event entry events.set(fullTopic, { topic: fullTopic, parts: parts, count: 1, firstSeen: new Date().toISOString(), lastSeen: new Date().toISOString(), lastData: data }); } // Use set to trigger change notification this.set('events', events); this.set('lastUpdateTime', new Date().toISOString()); } ``` ## Graph Construction ### Node Creation The graph construction process follows two passes: 1. **First Pass - Node Creation**: - Creates nodes for each unique topic segment - Preserves existing node positions if they've been dragged - New nodes are positioned near their parent in the hierarchy 2. **Second Pass - Event Counting**: - Counts occurrences of each topic segment - Updates node counts to reflect event frequency ### Link Construction Links are created as chains representing complete topic paths: ```283:342:spore-ui/public/scripts/components/EventComponent.js // Build links as chains for each topic // For "cluster/update", create: center -> cluster -> update this.graphLinks = []; const linkSet = new Set(); // Track links to avoid duplicates if (events && events.size > 0) { for (const [topic, data] of events) { const parts = data.parts; if (parts.length === 0) continue; // Connect center to first part const firstPart = parts[0]; const centerToFirst = `center-${firstPart}`; if (!linkSet.has(centerToFirst)) { this.graphLinks.push({ source: 'center', target: firstPart, type: 'center-link', count: data.count, topic: topic }); linkSet.add(centerToFirst); } else { // Update count for existing link const existingLink = this.graphLinks.find(l => `${l.source}-${l.target}` === centerToFirst ); if (existingLink) { existingLink.count += data.count; } } // Connect each part to the next (creating a chain) for (let i = 0; i < parts.length - 1; i++) { const source = parts[i]; const target = parts[i + 1]; const linkKey = `${source}-${target}`; if (!linkSet.has(linkKey)) { this.graphLinks.push({ source: source, target: target, type: 'topic-link', topic: topic, count: data.count }); linkSet.add(linkKey); } else { // Update count for existing link const existingLink = this.graphLinks.find(l => `${l.source}-${l.target}` === linkKey ); if (existingLink) { existingLink.count += data.count; } } } } } ``` ## Force Simulation The D3.js force simulation provides the physics-based layout: ```103:111:spore-ui/public/scripts/components/EventComponent.js // Initialize simulation this.simulation = d3.forceSimulation() .force('link', d3.forceLink().id(d => d.id).distance(100)) .force('charge', d3.forceManyBody().strength(-300)) .force('center', d3.forceCenter(this.width / 2, this.height / 2)) .force('collision', d3.forceCollide().radius(35)) .alphaDecay(0.0228) // Slower decay to allow simulation to run longer .velocityDecay(0.4); // Higher velocity decay for smoother, less jumpy movement ``` ### Force Parameters - **Link Force**: Maintains 100px distance between connected nodes - **Charge Force**: -300 strength creates repulsion between nodes - **Center Force**: Keeps the graph centered in the viewport - **Collision Detection**: Prevents nodes from overlapping (35px collision radius) - **Decay Rate**: Slow decay (0.0228) keeps the simulation running smoothly - **Velocity Decay**: High decay (0.4) prevents excessive movement ## Animation System ### Event Arrival Animation When a new event arrives, a golden animation dot travels along the event chain: ```725:809:spore-ui/public/scripts/components/EventComponent.js animateEventMessage(topic, data) { const g = this.svg.select('g'); if (g.empty()) return; const parts = data.parts || topic.split('/').filter(p => p); if (parts.length === 0) return; // Wait for the next tick to ensure nodes exist setTimeout(() => { // Build the chain: center -> first -> second -> ... -> last const chain = ['center', ...parts]; // Animate along each segment of the chain let delay = 0; for (let i = 0; i < chain.length - 1; i++) { const sourceId = chain[i]; const targetId = chain[i + 1]; const sourceNode = this.graphNodes.find(n => n.id === sourceId); const targetNode = this.graphNodes.find(n => n.id === targetId); if (!sourceNode || !targetNode) continue; setTimeout(() => { this.animateDotAlongLink(sourceNode, targetNode, g); }, delay); // Add delay between segments (staggered animation) delay += 400; // Duration (300ms) + small gap (100ms) } }, 100); } animateDotAlongLink(sourceNode, targetNode, g) { if (!sourceNode || !targetNode || !g) return; // Calculate positions const dx = targetNode.x - sourceNode.x; const dy = targetNode.y - sourceNode.y; const length = Math.sqrt(dx * dx + dy * dy); if (length === 0) return; // Normalize direction const nx = dx / length; const ny = dy / length; // Calculate node radii const sourceRadius = sourceNode.type === 'center' ? 18 : 14; const targetRadius = targetNode.type === 'center' ? 18 : 14; // Start from edge of source node const startX = sourceNode.x + nx * sourceRadius; const startY = sourceNode.y + ny * sourceRadius; // End at edge of target node const endX = targetNode.x - nx * targetRadius; const endY = targetNode.y - ny * targetRadius; // Create animation dot const animationGroup = g.append('g').attr('class', 'animation-group'); const dot = animationGroup.append('circle') .attr('r', 4) .attr('fill', '#FFD700') .attr('stroke', '#fff') .attr('stroke-width', 2) .attr('cx', startX) .attr('cy', startY); // Animate the dot along the path dot.transition() .duration(300) .ease(d3.easeLinear) .attr('cx', endX) .attr('cy', endY) .on('end', function() { // Fade out after reaching destination dot.transition() .duration(100) .style('opacity', 0) .remove(); animationGroup.remove(); }); } ``` ### Animation Characteristics - **Golden Dot**: Visual indicator traveling along event chains - **Staggered Timing**: Each segment of a multi-part topic animates sequentially - **Duration**: 300ms per segment with 100ms delay between segments - **Smooth Motion**: Linear easing for consistent speed ## WebSocket Integration The Events view subscribes to all WebSocket messages through the gateway: ```1256:1280:spore-ui/public/scripts/view-models.js // Set up WebSocket event listeners setupWebSocketListeners() { if (!window.wsClient) { // Retry after a short delay to allow wsClient to initialize setTimeout(() => this.setupWebSocketListeners(), 1000); return; } // Listen for all websocket messages window.wsClient.on('message', (data) => { const topic = data.topic || data.type; if (topic) { this.addTopic(topic, data); } }); // Listen for connection status changes window.wsClient.on('connected', () => { logger.info('EventViewModel: WebSocket connected'); }); window.wsClient.on('disconnected', () => { logger.debug('EventViewModel: WebSocket disconnected'); }); } ``` ### Event Types Tracked All WebSocket messages are captured and displayed, including: - **cluster/update**: Node discovery and cluster membership changes - **cluster/event**: Arbitrary events from cluster members - **api/tasks**: Task execution notifications - **api/config**: Configuration updates - **node/***: Node-specific events - Any custom topics sent by SPORE nodes ## Empty State When no events have been received, the view displays a helpful message: ```604:632:spore-ui/public/scripts/components/EventComponent.js showEmptyState() { if (!this.isInitialized || !this.svg) return; const g = this.svg.select('g'); if (g.empty()) return; // Remove any existing message g.selectAll('.empty-state').remove(); // Account for the initial zoom transform (scale(1.4) translate(-200, -150)) // We need to place the text in the center of the transformed coordinate space const transformX = -200; const transformY = -150; const scale = 1.4; // Calculate centered position in transformed space const x = ((this.width / 2) - transformX) / scale; const y = ((this.height / 2) - transformY) / scale; const emptyMsg = g.append('text') .attr('class', 'empty-state') .attr('x', x) .attr('y', y) .attr('text-anchor', 'middle') .attr('font-size', '18px') .attr('fill', 'var(--text-secondary)') .attr('font-weight', '500') .text('Waiting for websocket events...'); } ``` ## Best Practices ### Performance Considerations 1. **Event Accumulation**: Events accumulate over time; consider clearing the view for long-running sessions 2. **Many Events**: With hundreds of unique topics, the graph may become cluttered 3. **Animation**: Rapid event bursts may cause overlapping animations ### Usage Tips 1. **Rearrange Regularly**: Use the rearrange button to clean up the layout after many events 2. **Drag Center Node**: Move the entire graph to see different areas of the visualization 3. **Zoom Out**: Zoom out to see the overall event pattern across the cluster 4. **Interactive Exploration**: Hover over links to see event frequencies ## Troubleshooting ### No Events Showing - **Check WebSocket Connection**: Verify the gateway WebSocket is connected - **Check Node Activity**: Ensure SPORE nodes are actively sending events - **Browser Console**: Check for any JavaScript errors ### Graph Not Updating - **Force Refresh**: Reload the page to reinitialize the WebSocket connection - **Check Gateway**: Verify the SPORE Gateway is running and accessible ### Performance Issues - **Clear Events**: Reload the page to clear accumulated events - **Reduce Events**: Limit the event rate in your SPORE nodes - **Browser Settings**: Ensure hardware acceleration is enabled ## Technical Details ### Dependencies - **D3.js v7**: Force-directed graph simulation and SVG manipulation - **SPORE Gateway**: WebSocket connection to cluster event stream - **Component Framework**: Built on the SPORE UI component architecture ### Browser Compatibility - **Modern Browsers**: Chrome, Firefox, Safari, Edge (latest versions) - **SVG Support**: Requires modern browser with full SVG support - **WebSocket Support**: Native WebSocket API required ### View Structure ```254:262:spore-ui/public/index.html