🔌 Conexión WebSocket
class MusicDashboard {
constructor() {
this.ws = null;
this.isConnected = false;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.charts = {};
this.dataHistory = {
rpm: [],
bpm: [],
genres: {},
artists: {}
};
}
connect() {
try {
// Conectar a WebSocket
this.ws = new WebSocket('ws://localhost:8080/music-stream');
this.ws.onopen = () => {
console.log('WebSocket connected');
this.isConnected = true;
this.reconnectAttempts = 0;
this.updateConnectionStatus('connected');
this.addStreamEntry('Conexión establecida', 'success');
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.processData(data);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
this.addStreamEntry('Error en conexión', 'error');
};
this.ws.onclose = () => {
console.log('WebSocket disconnected');
this.isConnected = false;
this.updateConnectionStatus('disconnected');
// Intentar reconectar
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
setTimeout(() => this.connect(), 3000 * this.reconnectAttempts);
this.addStreamEntry(`Reconectando... intento ${this.reconnectAttempts}`, 'warning');
}
};
} catch (error) {
console.error('Failed to connect:', error);
this.addStreamEntry('Error al conectar: ' + error.message, 'error');
}
}
disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
processData(data) {
// Procesar diferentes tipos de datos
switch (data.type) {
case 'rpm':
this.updateRPM(data.value, data.timestamp);
break;
case 'bpm':
this.updateBPM(data.value, data.timestamp);
break;
case 'genre':
this.updateGenre(data.genre, data.count);
break;
case 'artist':
this.updateArtist(data.artist, data.count);
break;
}
// Agregar entrada al stream
this.addStreamEntry(`${data.type.toUpperCase()}: ${JSON.stringify(data)}`, 'info');
}
updateRPM(value, timestamp) {
this.dataHistory.rpm.push({ value, timestamp });
// Mantener solo últimos 60 valores
if (this.dataHistory.rpm.length > 60) {
this.dataHistory.rpm.shift();
}
// Actualizar UI
document.getElementById('rpmValue').textContent = Math.round(value);
// Actualizar gráfico
if (this.charts.rpm) {
this.updateLineChart(this.charts.rpm, this.dataHistory.rpm);
}
}
updateBPM(value, timestamp) {
this.dataHistory.bpm.push({ value, timestamp });
if (this.dataHistory.bpm.length > 60) {
this.dataHistory.bpm.shift();
}
document.getElementById('bpmValue').textContent = Math.round(value);
if (this.charts.bpm) {
this.updateLineChart(this.charts.bpm, this.dataHistory.bpm);
}
}
updateGenre(genre, count) {
this.dataHistory.genres[genre] = count;
if (this.charts.genre) {
this.updateBarChart(this.charts.genre, this.dataHistory.genres);
}
}
updateArtist(artist, count) {
this.dataHistory.artists[artist] = count;
if (this.charts.artists) {
this.updateBarChart(this.charts.artists, this.dataHistory.artists);
}
}
updateConnectionStatus(status) {
const indicator = document.getElementById('connectionStatus');
const indicators = document.querySelectorAll('.status-indicator');
indicators.forEach(ind => {
ind.className = `status-indicator status-${status}`;
});
indicator.innerHTML = `
${status === 'connected' ? 'Conectado' : status === 'disconnected' ? 'Desconectado' : 'Sincronizando'}
`;
}
addStreamEntry(message, type) {
const stream = document.getElementById('dataStream');
const entry = document.createElement('div');
entry.className = 'stream-entry';
entry.style.borderLeftColor = type === 'error' ? '#f44336' : type === 'success' ? '#4caf50' : '#ff9800';
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
stream.appendChild(entry);
stream.scrollTop = stream.scrollHeight;
// Limitar entradas
if (stream.children.length > 100) {
stream.removeChild(stream.firstChild);
}
}
}
📊 Configuración de Gráficos con Chart.js
class ChartManager {
constructor() {
this.charts = {};
this.chartColors = {
primary: '#bb86fc',
secondary: '#03dac6',
accent: '#ff6b6b',
warning: '#ff9800',
success: '#4caf50'
};
}
initCharts() {
// Gráfico de línea para RPM
this.charts.rpm = new Chart(document.getElementById('rpmChart'), {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'RPM',
data: [],
borderColor: this.chartColors.primary,
backgroundColor: this.chartColors.primary + '20',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
x: { display: false },
y: {
beginAtZero: true,
max: 200,
ticks: { color: '#e0e0e0' },
grid: { color: '#333' }
}
}
}
});
// Gráfico de línea para BPM
this.charts.bpm = new Chart(document.getElementById('bpmChart'), {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'BPM',
data: [],
borderColor: this.chartColors.secondary,
backgroundColor: this.chartColors.secondary + '20',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
x: { display: false },
y: {
min: 60,
max: 200,
ticks: { color: '#e0e0e0' },
grid: { color: '#333' }
}
}
}
});
// Gráfico de barras para géneros
this.charts.genre = new Chart(document.getElementById('genreChart'), {
type: 'doughnut',
data: {
labels: [],
datasets: [{
data: [],
backgroundColor: [
this.chartColors.primary,
this.chartColors.secondary,
this.chartColors.accent,
this.chartColors.warning,
this.chartColors.success
]
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: { color: '#e0e0e0' }
}
}
}
});
// Gráfico de barras para artistas
this.charts.artists = new Chart(document.getElementById('artistsChart'), {
type: 'bar',
data: {
labels: [],
datasets: [{
label: 'Reproducciones',
data: [],
backgroundColor: this.chartColors.primary
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false }
},
scales: {
x: {
ticks: {
color: '#e0e0e0',
maxRotation: 45
},
grid: { display: false }
},
y: {
ticks: { color: '#e0e0e0' },
grid: { color: '#333' }
}
}
}
});
}
updateLineChart(chart, data) {
const labels = data.map((_, index) => index);
const values = data.map(item => item.value);
chart.data.labels = labels;
chart.data.datasets[0].data = values;
chart.update('none'); // Update sin animación para mejor performance
}
updateBarChart(chart, data) {
const sortedData = Object.entries(data)
.sort(([,a], [,b]) => b - a)
.slice(0, 10);
chart.data.labels = sortedData.map(([key]) => key);
chart.data.datasets[0].data = sortedData.map(([, value]) => value);
chart.update('none');
}
updateDoughnutChart(chart, data) {
const sortedData = Object.entries(data)
.sort(([,a], [,b]) => b - a)
.slice(0, 5);
chart.data.labels = sortedData.map(([key]) => key);
chart.data.datasets[0].data = sortedData.map(([, value]) => value);
chart.update('none');
}
}