Files
leox-gpon-monitoring/static/js/charts.js
Mateusz Gruszczyński 69711b46bc release
2026-01-02 22:31:35 +01:00

693 lines
26 KiB
JavaScript

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();
});