Avg. $/Stream
$${avgRevenuePerStream.toFixed(6)}
`;
};
const renderDataTable = () => {
dataTableBody.innerHTML = '';
state.genres.forEach(g => {
const row = `
| ${g.name} |
${formatNumber(g.streams)} |
${formatNumber(g.listeners)} |
${formatCurrency(g.revenue)} |
`;
dataTableBody.innerHTML += row;
});
};
const renderCharts = () => {
// Genre Doughnut Chart
if (genreChart) genreChart.destroy();
genreChart = new Chart(genreCanvas.getContext('2d'), {
type: 'doughnut',
data: {
labels: state.genres.map(g => g.name),
datasets: [{
data: state.genres.map(g => g.streams),
backgroundColor: ['#8b5cf6', '#ec4899', '#3b82f6', '#10b981', '#f97316'],
}]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top' } } }
});
// Top Artists Bar Chart
const allArtists = state.genres.flatMap(g => g.artists);
allArtists.sort((a, b) => b.streams - a.streams);
const topArtists = allArtists.slice(0, 5);
if (artistChart) artistChart.destroy();
artistChart = new Chart(artistCanvas.getContext('2d'), {
type: 'bar',
data: {
labels: topArtists.map(a => a.name),
datasets: [{
label: 'Streams',
data: topArtists.map(a => a.streams),
backgroundColor: 'rgba(124, 58, 237, 0.7)',
borderColor: 'rgba(124, 58, 237, 1)',
borderWidth: 1
}]
},
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: { x: { ticks: { callback: (value) => formatNumber(value) } } }
}
});
};
const renderConfigRows = () => {
configRowsContainer.innerHTML = '';
state.genres.forEach(g => {
let artistInputs = '';
g.artists.forEach(a => {
artistInputs += `
`;
});
const genreBlock = `
`;
configRowsContainer.innerHTML += genreBlock;
});
addConfigEventListeners();
};
// --- EVENT HANDLERS ---
const handleConfigChange = (e) => {
const genreId = parseInt(e.target.dataset.genreId);
const field = e.target.dataset.field;
const genre = state.genres.find(g => g.id === genreId);
if (!genre) return;
if (e.target.classList.contains('config-genre-input')) {
genre[field] = field === 'name' ? e.target.value : parseFloat(e.target.value) || 0;
} else if (e.target.classList.contains('config-artist-input')) {
const artistId = parseInt(e.target.dataset.artistId);
const artist = genre.artists.find(a => a.id === artistId);
if (artist) {
artist[field] = field === 'name' ? e.target.value : parseFloat(e.target.value) || 0;
}
}
renderAll();
};
const handleAddGenre = () => {
const newId = state.genres.length > 0 ? Math.max(...state.genres.map(g => g.id)) + 1 : 1;
state.genres.push({ id: newId, name: "New Genre", streams: 0, listeners: 0, revenue: 0, artists: [] });
renderAll();
};
const handleRemoveGenre = (e) => {
const genreId = parseInt(e.target.dataset.genreId);
state.genres = state.genres.filter(g => g.id !== genreId);
renderAll();
};
const handleAddArtist = (e) => {
const genreId = parseInt(e.target.dataset.genreId);
const genre = state.genres.find(g => g.id === genreId);
if (genre) {
const newArtistId = genre.artists.length > 0 ? Math.max(...genre.artists.map(a => a.id)) + 1 : genreId * 100 + 1;
genre.artists.push({ id: newArtistId, name: "New Artist", streams: 0 });
}
renderAll();
};
const handleRemoveArtist = (e) => {
const genreId = parseInt(e.target.dataset.genreId);
const artistId = parseInt(e.target.dataset.artistId);
const genre = state.genres.find(g => g.id === genreId);
if (genre) {
genre.artists = genre.artists.filter(a => a.id !== artistId);
}
renderAll();
};
const addConfigEventListeners = () => {
document.querySelectorAll('.config-genre-input, .config-artist-input').forEach(input => input.addEventListener('change', handleConfigChange));
document.querySelectorAll('.remove-genre-btn').forEach(btn => btn.addEventListener('click', handleRemoveGenre));
document.querySelectorAll('.add-artist-btn').forEach(btn => btn.addEventListener('click', handleAddArtist));
document.querySelectorAll('.remove-artist-btn').forEach(btn => btn.addEventListener('click', handleRemoveArtist));
};
const handleDownloadPdf = () => {
const { jsPDF } = window.jspdf;
const pdfContent = document.getElementById('pdf-content');
// Hide non-PDF elements
document.querySelectorAll('.no-print').forEach(el => el.style.visibility = 'hidden');
// Temporarily disable chart animations for clean capture
Chart.defaults.animation = false;
html2canvas(pdfContent, { scale: 2, useCORS: true, backgroundColor: '#ffffff' }).then(canvas => {
// Restore visibility and animations
document.querySelectorAll('.no-print').forEach(el => el.style.visibility = 'visible');
Chart.defaults.animation = true;
const imgData = canvas.toDataURL('image/png');
const pdf = new jsPDF({ orientation: 'p', unit: 'mm', format: 'a4' });
const pdfWidth = pdf.internal.pageSize.getWidth();
const imgWidth = pdfWidth - 20; // with margin
const imgHeight = canvas.height * imgWidth / canvas.width;
pdf.addImage(imgData, 'PNG', 10, 10, imgWidth, imgHeight);
pdf.save('Music-Streaming-Trends.pdf');
});
};
// --- TABBING LOGIC ---
let currentTabIndex = 0;
const updateTabButtons = () => {
prevTabBtn.disabled = currentTabIndex === 0;
nextTabBtn.disabled = currentTabIndex === tabContents.length - 1;
};
const switchTab = (index) => {
tabButtons.forEach(btn => btn.classList.remove('active'));
tabContents.forEach(content => content.classList.remove('active'));
tabButtons[index].classList.add('active');
tabContents[index].classList.add('active');
currentTabIndex = index;
updateTabButtons();
};
tabButtons.forEach((button, index) => button.addEventListener('click', () => switchTab(index)));
prevTabBtn.addEventListener('click', () => { if (currentTabIndex > 0) switchTab(currentTabIndex - 1); });
nextTabBtn.addEventListener('click', () => { if (currentTabIndex < tabContents.length - 1) switchTab(currentTabIndex + 1); });
// --- INITIALIZATION ---
if (kpiCardsContainer && addGenreBtn && downloadPdfBtn) {
addGenreBtn.addEventListener('click', handleAddGenre);
downloadPdfBtn.addEventListener('click', handleDownloadPdf);
renderAll();
updateTabButtons();
} else {
console.error("Essential dashboard elements could not be found.");
}
});