WebSockets & Real-Time Apps

Real-Time Data Visualization

18 min Lesson 16 of 35

Real-Time Data Visualization

Real-time data visualization transforms streaming data into dynamic, interactive visual representations. WebSockets enable live dashboards that update instantly without page refreshes, providing users with up-to-the-second insights.

Building a Live Dashboard

Let's create a real-time analytics dashboard that displays live metrics:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Live Dashboard</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> .dashboard { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; padding: 20px; } .metric-card { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .metric-value { font-size: 2.5rem; font-weight: bold; color: #2563eb; } .metric-label { color: #666; font-size: 0.9rem; } </style> </head> <body> <div class="dashboard"> <div class="metric-card"> <div class="metric-value" id="activeUsers">0</div> <div class="metric-label">Active Users</div> </div> <div class="metric-card"> <div class="metric-value" id="requestsPerMin">0</div> <div class="metric-label">Requests/Min</div> </div> <div class="metric-card"> <canvas id="trafficChart"></canvas> </div> </div> <script> const ws = new WebSocket('ws://localhost:8080'); ws.onopen = () => { console.log('Connected to analytics stream'); ws.send(JSON.stringify({ type: 'subscribe', channel: 'analytics' })); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); updateDashboard(data); }; function updateDashboard(data) { if (data.activeUsers) { animateValue('activeUsers', data.activeUsers); } if (data.requestsPerMin) { animateValue('requestsPerMin', data.requestsPerMin); } if (data.traffic) { updateTrafficChart(data.traffic); } } function animateValue(id, value) { const element = document.getElementById(id); const current = parseInt(element.textContent); const increment = (value - current) / 20; let step = 0; const timer = setInterval(() => { step++; element.textContent = Math.round(current + (increment * step)); if (step >= 20) clearInterval(timer); }, 50); } </script> </body> </html>

Streaming Chart Data with Chart.js

Chart.js integrates seamlessly with WebSockets for live charts:

// Initialize chart const ctx = document.getElementById('trafficChart').getContext('2d'); const trafficChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: 'Traffic (req/s)', data: [], borderColor: 'rgb(37, 99, 235)', backgroundColor: 'rgba(37, 99, 235, 0.1)', tension: 0.4, fill: true }] }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 750 }, scales: { x: { display: true, title: { display: true, text: 'Time' } }, y: { display: true, title: { display: true, text: 'Requests per Second' }, beginAtZero: true } } } }); function updateTrafficChart(traffic) { const now = new Date().toLocaleTimeString(); // Add new data point trafficChart.data.labels.push(now); trafficChart.data.datasets[0].data.push(traffic); // Keep only last 20 points if (trafficChart.data.labels.length > 20) { trafficChart.data.labels.shift(); trafficChart.data.datasets[0].data.shift(); } // Update chart trafficChart.update('none'); // Disable animation for smooth updates }
Performance Tip: Use chart.update('none') to disable animations when updating frequently (more than once per second) to maintain smooth performance.

Server-Side Data Streaming

The backend generates and broadcasts real-time metrics:

const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); // Store subscribed clients const analyticsSubscribers = new Set(); wss.on('connection', (ws) => { console.log('Client connected'); ws.on('message', (message) => { const data = JSON.parse(message); if (data.type === 'subscribe' && data.channel === 'analytics') { analyticsSubscribers.add(ws); console.log('Client subscribed to analytics'); // Send initial data ws.send(JSON.stringify({ activeUsers: getActiveUsers(), requestsPerMin: getRequestsPerMin(), traffic: getCurrentTraffic() })); } }); ws.on('close', () => { analyticsSubscribers.delete(ws); console.log('Client disconnected'); }); }); // Broadcast analytics data every 2 seconds setInterval(() => { const analyticsData = { activeUsers: getActiveUsers(), requestsPerMin: getRequestsPerMin(), traffic: getCurrentTraffic() }; analyticsSubscribers.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(JSON.stringify(analyticsData)); } }); }, 2000); // Simulated metric functions function getActiveUsers() { return Math.floor(Math.random() * 1000) + 500; } function getRequestsPerMin() { return Math.floor(Math.random() * 5000) + 2000; } function getCurrentTraffic() { return Math.floor(Math.random() * 100) + 50; }

Throttling Updates

Prevent overwhelming the UI with too many updates:

class DataThrottler { constructor(callback, delay = 1000) { this.callback = callback; this.delay = delay; this.lastUpdate = 0; this.pendingData = null; this.timer = null; } update(data) { this.pendingData = data; const now = Date.now(); if (now - this.lastUpdate >= this.delay) { this.flush(); } else if (!this.timer) { this.timer = setTimeout(() => { this.flush(); }, this.delay - (now - this.lastUpdate)); } } flush() { if (this.pendingData) { this.callback(this.pendingData); this.lastUpdate = Date.now(); this.pendingData = null; this.timer = null; } } } // Usage const dashboardThrottler = new DataThrottler(updateDashboard, 500); ws.onmessage = (event) => { const data = JSON.parse(event.data); dashboardThrottler.update(data); };

Data Aggregation Strategies

Aggregate data on the server to reduce bandwidth:

class MetricsAggregator { constructor(window = 5000) { this.window = window; this.data = []; } add(value, timestamp = Date.now()) { this.data.push({ value, timestamp }); this.cleanup(); } cleanup() { const cutoff = Date.now() - this.window; this.data = this.data.filter(d => d.timestamp > cutoff); } getAverage() { if (this.data.length === 0) return 0; const sum = this.data.reduce((acc, d) => acc + d.value, 0); return Math.round(sum / this.data.length); } getMax() { if (this.data.length === 0) return 0; return Math.max(...this.data.map(d => d.value)); } getMin() { if (this.data.length === 0) return 0; return Math.min(...this.data.map(d => d.value)); } getPercentile(p) { if (this.data.length === 0) return 0; const sorted = this.data.map(d => d.value).sort((a, b) => a - b); const index = Math.ceil((p / 100) * sorted.length) - 1; return sorted[index]; } } // Usage const trafficAggregator = new MetricsAggregator(5000); // Add data points as they come in app.use((req, res, next) => { trafficAggregator.add(1); next(); }); // Broadcast aggregated data setInterval(() => { const stats = { avgTraffic: trafficAggregator.getAverage(), maxTraffic: trafficAggregator.getMax(), p95Traffic: trafficAggregator.getPercentile(95) }; broadcast(stats); }, 2000);
Note: Aggregation reduces data transmission by up to 90% while maintaining statistical accuracy. Send summaries instead of raw data points.

D3.js Integration

Use D3.js for advanced real-time visualizations:

const margin = { top: 20, right: 20, bottom: 30, left: 50 }; const width = 600 - margin.left - margin.right; const height = 400 - margin.top - margin.bottom; const svg = d3.select('#chart') .append('svg') .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom) .append('g') .attr('transform', `translate(${margin.left},${margin.top})`); const data = []; const maxDataPoints = 50; const x = d3.scaleTime().range([0, width]); const y = d3.scaleLinear().range([height, 0]); const line = d3.line() .x(d => x(d.timestamp)) .y(d => y(d.value)) .curve(d3.curveMonotoneX); const path = svg.append('path') .datum(data) .attr('class', 'line') .attr('fill', 'none') .attr('stroke', 'steelblue') .attr('stroke-width', 2); const xAxis = svg.append('g') .attr('transform', `translate(0,${height})`); const yAxis = svg.append('g'); function updateChart(newValue) { data.push({ timestamp: new Date(), value: newValue }); if (data.length > maxDataPoints) { data.shift(); } x.domain(d3.extent(data, d => d.timestamp)); y.domain([0, d3.max(data, d => d.value) * 1.1]); path.datum(data) .transition() .duration(300) .attr('d', line); xAxis.transition().duration(300).call(d3.axisBottom(x)); yAxis.transition().duration(300).call(d3.axisLeft(y)); } ws.onmessage = (event) => { const data = JSON.parse(event.data); updateChart(data.value); };

Updating DOM Efficiently

Optimize DOM updates for smooth real-time rendering:

class DOMBatcher { constructor() { this.updates = []; this.rafId = null; } schedule(callback) { this.updates.push(callback); if (!this.rafId) { this.rafId = requestAnimationFrame(() => { this.flush(); }); } } flush() { this.updates.forEach(callback => callback()); this.updates = []; this.rafId = null; } } const batcher = new DOMBatcher(); ws.onmessage = (event) => { const data = JSON.parse(event.data); batcher.schedule(() => { document.getElementById('metric1').textContent = data.metric1; document.getElementById('metric2').textContent = data.metric2; document.getElementById('metric3').textContent = data.metric3; }); };
Warning: Avoid updating the DOM on every WebSocket message if messages arrive faster than 60 times per second. Batch updates using requestAnimationFrame.

Exercise: Live Stock Ticker

Build a Real-Time Stock Ticker:
  1. Create a WebSocket server that streams simulated stock prices every 500ms
  2. Display 5 stocks with color-coded price changes (green for up, red for down)
  3. Show a sparkline chart for each stock using Canvas API
  4. Implement a throttling mechanism to update UI at most 10 times per second
  5. Add a rolling 24-hour high/low display
  6. Bonus: Add sound alerts when a stock moves more than 5%

Summary

  • Real-time dashboards provide instant insights without page refreshes
  • Chart.js and D3.js integrate well with WebSockets for live charts
  • Throttle updates to prevent UI overload (use requestAnimationFrame)
  • Aggregate data on the server to reduce bandwidth usage
  • Batch DOM updates for better performance
  • Consider user experience: smooth animations and meaningful transitions