class GPONCharts { constructor() { this.charts = {}; this.init(); } init() { this.createOpticalChart(); this.createTemperatureChart(); this.createTrafficChart(); this.createBytesChart(); this.createVolumeChart(); this.createFECChart(); this.setupGlobalPeriodSelector(); document.getElementById('period-optical').addEventListener('change', (e) => { this.updateOpticalChart(e.target.value); }); document.getElementById('period-temperature').addEventListener('change', (e) => { this.updateTemperatureChart(e.target.value); }); document.getElementById('period-traffic').addEventListener('change', (e) => { this.updateTrafficChart(e.target.value); }); document.getElementById('period-bytes').addEventListener('change', (e) => { this.updateBytesChart(e.target.value); }); document.getElementById('period-volume').addEventListener('change', (e) => { this.updateVolumeChart(e.target.value); }); document.getElementById('period-fec').addEventListener('change', (e) => { this.updateFECChart(e.target.value); }); document.querySelectorAll('input[name="volume-chart-type"]').forEach(radio => { radio.addEventListener('change', (e) => { this.changeVolumeChartType(e.target.value); }); }); this.updateOpticalChart('1h'); this.updateTemperatureChart('1h'); this.updateTrafficChart('1h'); this.updateBytesChart('1h'); this.updateVolumeChart('1h'); this.updateFECChart('1h'); setInterval(() => { const optPeriod = document.getElementById('period-optical').value; const tempPeriod = document.getElementById('period-temperature').value; const trafficPeriod = document.getElementById('period-traffic').value; const bytesPeriod = document.getElementById('period-bytes').value; const volumePeriod = document.getElementById('period-volume').value; const fecPeriod = document.getElementById('period-fec').value; this.updateOpticalChart(optPeriod); this.updateTemperatureChart(tempPeriod); this.updateTrafficChart(trafficPeriod); this.updateBytesChart(bytesPeriod); this.updateVolumeChart(volumePeriod); this.updateFECChart(fecPeriod); }, 30000); console.log('[Charts] Initialized'); } setupGlobalPeriodSelector() { const radios = document.querySelectorAll('input[name="global-period"]'); radios.forEach(radio => { radio.addEventListener('change', (e) => { const period = e.target.value; document.getElementById('period-optical').value = period; document.getElementById('period-temperature').value = period; document.getElementById('period-traffic').value = period; document.getElementById('period-bytes').value = period; document.getElementById('period-volume').value = period; document.getElementById('period-fec').value = period; this.updateOpticalChart(period); this.updateTemperatureChart(period); this.updateTrafficChart(period); this.updateBytesChart(period); this.updateVolumeChart(period); this.updateFECChart(period); console.log('[Charts] Global period: ' + period); }); }); } createOpticalChart() { const ctx = document.getElementById('chart-optical').getContext('2d'); this.charts.optical = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [ { label: 'RX Power (dBm)', data: [], borderColor: '#00ccff', backgroundColor: 'rgba(0, 204, 255, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: 0 }, { label: 'TX Power (dBm)', data: [], borderColor: '#ffcc00', backgroundColor: 'rgba(255, 204, 0, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: 0 } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { display: true, labels: { color: '#ffffff' } }, tooltip: { mode: 'index', intersect: false } }, scales: { x: { ticks: { color: '#b0b0b0', maxRotation: 0 }, grid: { color: 'rgba(255, 255, 255, 0.1)' } }, y: { ticks: { color: '#b0b0b0' }, grid: { color: 'rgba(255, 255, 255, 0.1)' }, title: { display: true, text: 'dBm', color: '#b0b0b0' } } } } }); } createTemperatureChart() { const ctx = document.getElementById('chart-temperature').getContext('2d'); this.charts.temperature = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [{ label: 'Temperature (°C)', data: [], borderColor: '#ff6b6b', backgroundColor: 'rgba(255, 107, 107, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, labels: { color: '#ffffff' } }, tooltip: { callbacks: { label: function(context) { return context.parsed.y ? context.parsed.y.toFixed(1) + '°C' : 'N/A'; } } } }, scales: { x: { ticks: { color: '#b0b0b0', maxRotation: 0 }, grid: { color: 'rgba(255, 255, 255, 0.1)' } }, y: { ticks: { color: '#b0b0b0' }, grid: { color: 'rgba(255, 255, 255, 0.1)' }, title: { display: true, text: '°C', color: '#b0b0b0' } } } } }); } createTrafficChart() { const ctx = document.getElementById('chart-traffic').getContext('2d'); this.charts.traffic = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [ { label: 'RX Packets/s', data: [], borderColor: '#4bc0c0', backgroundColor: 'rgba(75, 192, 192, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: 0 }, { label: 'TX Packets/s', data: [], borderColor: '#ff6384', backgroundColor: 'rgba(255, 99, 132, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: 0 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, labels: { color: '#ffffff' } }, tooltip: { mode: 'index', intersect: false } }, scales: { x: { ticks: { color: '#b0b0b0', maxRotation: 0 }, grid: { color: 'rgba(255, 255, 255, 0.1)' } }, y: { ticks: { color: '#b0b0b0' }, grid: { color: 'rgba(255, 255, 255, 0.1)' }, title: { display: true, text: 'packets/s', color: '#b0b0b0' } } } } }); } createBytesChart() { const ctx = document.getElementById('chart-bytes').getContext('2d'); this.charts.bytes = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [ { label: 'RX (Mb/s)', data: [], borderColor: '#4bc0c0', backgroundColor: 'rgba(75, 192, 192, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: 0 }, { label: 'TX (Mb/s)', data: [], borderColor: '#ff6384', backgroundColor: 'rgba(255, 99, 132, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: 0 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, labels: { color: '#ffffff' } }, tooltip: { mode: 'index', intersect: false, callbacks: { label: function(context) { return context.dataset.label + ': ' + (context.parsed.y ? context.parsed.y.toFixed(2) : '0') + ' Mb/s'; } } } }, scales: { x: { ticks: { color: '#b0b0b0', maxRotation: 0 }, grid: { color: 'rgba(255, 255, 255, 0.1)' } }, y: { ticks: { color: '#b0b0b0', callback: function(value) { return value.toFixed(1); } }, grid: { color: 'rgba(255, 255, 255, 0.1)' }, title: { display: true, text: 'Mb/s', color: '#b0b0b0' } } } } }); } createVolumeChart() { const ctx = document.getElementById('chart-volume').getContext('2d'); const self = this; this.charts.volume = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [ { label: 'RX per minute', data: [], borderColor: '#4bc0c0', backgroundColor: 'rgba(75, 192, 192, 0.05)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: 0 }, { label: 'TX per minute', data: [], borderColor: '#ff6384', backgroundColor: 'rgba(255, 99, 132, 0.05)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: 0 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, labels: { color: '#ffffff' } }, tooltip: { mode: 'index', intersect: false, callbacks: { label: function(context) { const bytes = context.parsed.y; const formatted = self.formatBytes(bytes); return context.dataset.label + ': ' + formatted; } } } }, scales: { x: { stacked: false, ticks: { color: '#b0b0b0', maxRotation: 0 }, grid: { color: 'rgba(255, 255, 255, 0.1)' } }, y: { stacked: false, ticks: { color: '#b0b0b0', callback: function(value) { return self.formatBytes(value); } }, grid: { color: 'rgba(255, 255, 255, 0.1)' }, title: { display: true, text: 'Data per minute', color: '#b0b0b0' } } } } }); } createFECChart() { const ctx = document.getElementById('chart-fec').getContext('2d'); this.charts.fec = new Chart(ctx, { type: 'line', data: { labels: [], datasets: [ { label: 'FEC Corrected', data: [], borderColor: '#ffa500', backgroundColor: 'rgba(255, 165, 0, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: 0 }, { label: 'FEC Uncorrected', data: [], borderColor: '#ff0000', backgroundColor: 'rgba(255, 0, 0, 0.1)', borderWidth: 2, tension: 0.4, fill: true, pointRadius: 0 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, labels: { color: '#ffffff' } } }, scales: { x: { ticks: { color: '#b0b0b0', maxRotation: 0 }, grid: { color: 'rgba(255, 255, 255, 0.1)' } }, y: { ticks: { color: '#b0b0b0' }, grid: { color: 'rgba(255, 255, 255, 0.1)' }, title: { display: true, text: 'errors/s', color: '#b0b0b0' } } } } }); } changeVolumeChartType(type) { if (!this.charts.volume) return; console.log('[Charts] Changing volume chart to:', type); const currentData = { labels: this.charts.volume.data.labels, datasets: this.charts.volume.data.datasets }; this.charts.volume.destroy(); const ctx = document.getElementById('chart-volume').getContext('2d'); const self = this; const config = { type: type, data: { labels: currentData.labels, datasets: [ { label: 'RX per minute', data: currentData.datasets[0].data, borderColor: '#4bc0c0', backgroundColor: type === 'bar' ? 'rgba(75, 192, 192, 0.6)' : 'rgba(75, 192, 192, 0.05)', borderWidth: type === 'bar' ? 1 : 2, tension: 0.4, fill: true, pointRadius: 0 }, { label: 'TX per minute', data: currentData.datasets[1].data, borderColor: '#ff6384', backgroundColor: type === 'bar' ? 'rgba(255, 99, 132, 0.6)' : 'rgba(255, 99, 132, 0.05)', borderWidth: type === 'bar' ? 1 : 2, tension: 0.4, fill: true, pointRadius: 0 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, labels: { color: '#ffffff' } }, tooltip: { mode: 'index', intersect: false, callbacks: { label: function(context) { const bytes = context.parsed.y; const formatted = self.formatBytes(bytes); return context.dataset.label + ': ' + formatted; } } } }, scales: { x: { stacked: false, ticks: { color: '#b0b0b0', maxRotation: 0 }, grid: { color: 'rgba(255, 255, 255, 0.1)' } }, y: { stacked: false, ticks: { color: '#b0b0b0', callback: function(value) { return self.formatBytes(value); } }, grid: { color: 'rgba(255, 255, 255, 0.1)' }, title: { display: true, text: 'Data per minute', color: '#b0b0b0' } } } } }; this.charts.volume = new Chart(ctx, config); } async updateOpticalChart(period) { try { const response = await fetch('/api/history/optical/' + period); const data = await response.json(); if (!data || !data.timestamps || data.timestamps.length === 0) { console.warn('[Charts] No optical data'); return; } const labels = this.formatLabels(data.timestamps, period); this.charts.optical.data.labels = labels; this.charts.optical.data.datasets[0].data = data.data.rx_power || []; this.charts.optical.data.datasets[1].data = data.data.tx_power || []; this.charts.optical.update(); } catch (error) { console.error('[Charts] Error optical:', error); } } async updateTemperatureChart(period) { try { const response = await fetch('/api/history/optical/' + period); const data = await response.json(); if (!data || !data.timestamps || data.timestamps.length === 0) return; const labels = this.formatLabels(data.timestamps, period); this.charts.temperature.data.labels = labels; this.charts.temperature.data.datasets[0].data = data.data.temperature || []; this.charts.temperature.update(); } catch (error) { console.error('[Charts] Error temperature:', error); } } async updateTrafficChart(period) { try { const response = await fetch('/api/history/traffic/' + period); const data = await response.json(); if (!data || !data.timestamps || data.timestamps.length === 0) return; const labels = this.formatLabels(data.timestamps, period); const rxPps = data.data.rx_packets || []; const txPps = data.data.tx_packets || []; this.charts.traffic.data.labels = labels; this.charts.traffic.data.datasets[0].data = rxPps; this.charts.traffic.data.datasets[1].data = txPps; this.charts.traffic.update(); } catch (error) { console.error('[Charts] Error traffic:', error); } } async updateBytesChart(period) { try { const response = await fetch('/api/history/traffic/' + period); const data = await response.json(); if (!data || !data.timestamps || data.timestamps.length === 0) return; const labels = this.formatLabels(data.timestamps, period); const rxMbps = (data.data.rx_bytes || []).map(v => v !== null ? (v * 8) / 1000000 : null ); const txMbps = (data.data.tx_bytes || []).map(v => v !== null ? (v * 8) / 1000000 : null ); this.charts.bytes.data.labels = labels; this.charts.bytes.data.datasets[0].data = rxMbps; this.charts.bytes.data.datasets[1].data = txMbps; this.charts.bytes.update(); console.log('[Charts] Bytes - max RX Mb/s:', Math.max(...rxMbps.filter(v => v !== null)).toFixed(2)); } catch (error) { console.error('[Charts] Error bytes:', error); } } async updateVolumeChart(period) { try { const response = await fetch('/api/history/traffic/' + period); const data = await response.json(); if (!data || !data.timestamps || data.timestamps.length === 0) return; const labels = this.formatLabels(data.timestamps, period); const rxBytesRate = data.data.rx_bytes || []; const txBytesRate = data.data.tx_bytes || []; const step = data.step || 60; const rxPerInterval = rxBytesRate.map(rate => rate !== null ? rate * step : null ); const txPerInterval = txBytesRate.map(rate => rate !== null ? rate * step : null ); this.charts.volume.data.labels = labels; this.charts.volume.data.datasets[0].data = rxPerInterval; this.charts.volume.data.datasets[1].data = txPerInterval; this.charts.volume.update(); const maxRx = Math.max(...rxPerInterval.filter(v => v !== null)); console.log('[Charts] Volume - max per minute:', this.formatBytes(maxRx)); } catch (error) { console.error('[Charts] Error volume:', error); } } async updateFECChart(period) { try { const response = await fetch('/api/history/fec/' + period); const data = await response.json(); if (!data || !data.timestamps || data.timestamps.length === 0) return; const labels = this.formatLabels(data.timestamps, period); const correctedRate = data.data.corrected || []; const uncorrectedRate = data.data.uncorrected || []; this.charts.fec.data.labels = labels; this.charts.fec.data.datasets[0].data = correctedRate; this.charts.fec.data.datasets[1].data = uncorrectedRate; this.charts.fec.update(); } catch (error) { console.error('[Charts] Error FEC:', error); } } formatLabels(timestamps, period) { return timestamps.map(ts => { const date = new Date(ts * 1000); if (period === '7d' || period === '14d' || period === '30d' || period === '60d' || period === '90d') { return date.toLocaleDateString('pl-PL', { day: '2-digit', month: '2-digit' }) + ' ' + date.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }); } return date.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }); }); } formatBytes(bytes, decimals = 2) { if (bytes === null || bytes === undefined) return 'N/A'; if (bytes === 0) return '0 B'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } } document.addEventListener('DOMContentLoaded', () => { window.charts = new GPONCharts(); });