'use strict'; /** * Lightweight SVG sparkline chart component for WireGuard Dashboard * No external dependencies - pure SVG generation */ return { // Default options defaults: { width: 200, height: 50, strokeWidth: 2, fill: true, lineColor: '#0ea5e9', fillColor: 'rgba(14, 165, 233, 0.1)', gridColor: 'rgba(255, 255, 255, 0.1)', showGrid: false, animate: true }, /** * Generate SVG sparkline from data points * @param {number[]} data - Array of numeric values * @param {Object} opts - Chart options * @returns {string} SVG markup string */ sparkline: function(data, opts) { opts = Object.assign({}, this.defaults, opts || {}); if (!data || data.length < 2) { return this.emptyChart(opts); } var width = opts.width; var height = opts.height; var padding = 4; var chartWidth = width - padding * 2; var chartHeight = height - padding * 2; // Normalize data var max = Math.max.apply(null, data); var min = Math.min.apply(null, data); var range = max - min || 1; // Generate points var points = []; var step = chartWidth / (data.length - 1); for (var i = 0; i < data.length; i++) { var x = padding + i * step; var y = padding + chartHeight - ((data[i] - min) / range * chartHeight); points.push(x + ',' + y); } var pathData = 'M' + points.join(' L'); // Build fill path (closed area under line) var fillPath = ''; if (opts.fill) { fillPath = pathData + ' L' + (padding + chartWidth) + ',' + (padding + chartHeight) + ' L' + padding + ',' + (padding + chartHeight) + ' Z'; } // Build SVG var svg = ''; // Grid lines if (opts.showGrid) { for (var g = 0; g < 4; g++) { var gy = padding + (chartHeight / 3) * g; svg += ''; } } // Fill area if (opts.fill && fillPath) { svg += ''; } // Line svg += ''; svg += ''; return svg; }, /** * Generate dual-line chart for RX/TX comparison * @param {number[]} rxData - Download data points * @param {number[]} txData - Upload data points * @param {Object} opts - Chart options * @returns {string} SVG markup string */ dualSparkline: function(rxData, txData, opts) { opts = Object.assign({}, this.defaults, { width: 300, height: 60, rxColor: '#10b981', txColor: '#0ea5e9', rxFill: 'rgba(16, 185, 129, 0.1)', txFill: 'rgba(14, 165, 233, 0.1)' }, opts || {}); var width = opts.width; var height = opts.height; var padding = 4; var chartWidth = width - padding * 2; var chartHeight = height - padding * 2; // Combine for scale var allData = (rxData || []).concat(txData || []); if (allData.length < 2) { return this.emptyChart(opts); } var max = Math.max.apply(null, allData); var min = 0; var range = max - min || 1; var svg = ''; // Draw each line var datasets = [ { data: rxData, color: opts.rxColor, fill: opts.rxFill, label: 'RX' }, { data: txData, color: opts.txColor, fill: opts.txFill, label: 'TX' } ]; datasets.forEach(function(ds) { if (!ds.data || ds.data.length < 2) return; var step = chartWidth / (ds.data.length - 1); var points = []; for (var i = 0; i < ds.data.length; i++) { var x = padding + i * step; var y = padding + chartHeight - ((ds.data[i] - min) / range * chartHeight); points.push(x + ',' + y); } var pathData = 'M' + points.join(' L'); // Fill if (opts.fill) { var fillPath = pathData + ' L' + (padding + chartWidth) + ',' + (padding + chartHeight) + ' L' + padding + ',' + (padding + chartHeight) + ' Z'; svg += ''; } // Line svg += ''; }); // Legend svg += '↓ RX'; svg += '↑ TX'; svg += ''; return svg; }, /** * Generate empty/placeholder chart * @param {Object} opts - Chart options * @returns {string} SVG markup string */ emptyChart: function(opts) { var width = opts.width || 200; var height = opts.height || 50; return '' + '' + 'No data' + ''; }, /** * Estimate path length for animation */ estimatePathLength: function(points, width, height) { return Math.sqrt(width * width + height * height) * 1.5; }, /** * Create chart container element * @param {string} svgContent - SVG markup * @param {string} title - Chart title * @returns {HTMLElement} Container element */ createContainer: function(svgContent, title) { var container = document.createElement('div'); container.className = 'wg-chart-container'; if (title) { var titleEl = document.createElement('div'); titleEl.className = 'wg-chart-title'; titleEl.textContent = title; container.appendChild(titleEl); } var chartEl = document.createElement('div'); chartEl.className = 'wg-chart-body'; chartEl.innerHTML = svgContent; container.appendChild(chartEl); return container; }, /** * Traffic history ring buffer manager */ TrafficHistory: { maxPoints: 60, data: {}, add: function(ifaceName, rx, tx) { if (!this.data[ifaceName]) { this.data[ifaceName] = { rx: [], tx: [], timestamps: [] }; } var entry = this.data[ifaceName]; entry.rx.push(rx); entry.tx.push(tx); entry.timestamps.push(Date.now()); // Trim to max points if (entry.rx.length > this.maxPoints) { entry.rx.shift(); entry.tx.shift(); entry.timestamps.shift(); } }, get: function(ifaceName) { return this.data[ifaceName] || { rx: [], tx: [], timestamps: [] }; }, getRates: function(ifaceName) { var entry = this.data[ifaceName]; if (!entry || entry.rx.length < 2) return { rx: [], tx: [] }; var rxRates = []; var txRates = []; for (var i = 1; i < entry.rx.length; i++) { var timeDiff = (entry.timestamps[i] - entry.timestamps[i - 1]) / 1000; if (timeDiff > 0) { rxRates.push((entry.rx[i] - entry.rx[i - 1]) / timeDiff); txRates.push((entry.tx[i] - entry.tx[i - 1]) / timeDiff); } } return { rx: rxRates, tx: txRates }; }, clear: function(ifaceName) { if (ifaceName) { delete this.data[ifaceName]; } else { this.data = {}; } } } };