Dynamic Data Dashboard

Dynamic Data Dashboard

Sales Performance Overview

Select a data view to dynamically update the dashboard. All displayed values are editable directly.

${value}${metric.unit}

`; summaryMetricsContainer.appendChild(metricCard); }); // Render Charts chartsContainer.innerHTML = ''; // Destroy any existing Chart.js instances before re-rendering for (const chartId in activeCharts) { if (activeCharts[chartId]) { activeCharts[chartId].destroy(); delete activeCharts[chartId]; } } currentViewData.charts.forEach(chartConfig => { const chartDiv = document.createElement('div'); chartDiv.className = 'chart-container'; chartDiv.innerHTML = ``; chartsContainer.appendChild(chartDiv); const ctx = document.getElementById(chartConfig.id)?.getContext('2d'); if (ctx) { const labels = chartConfig.data.map(d => d.label); const values = chartConfig.data.map(d => d.value); let datasets = []; if (chartConfig.type === 'line') { datasets.push({ label: chartConfig.title, data: values, borderColor: chartConfig.borderColor || '#2b6cb0', backgroundColor: chartConfig.backgroundColor || 'rgba(43, 108, 176, 0.2)', fill: true, tension: 0.4 }); } else if (chartConfig.type === 'bar') { datasets.push({ label: chartConfig.title, data: values, backgroundColor: chartConfig.colors || ['#4299e1', '#48bb78', '#ecc94b', '#ed8936'], borderColor: '#ffffff', borderWidth: 1 }); } else if (chartConfig.type === 'pie' || chartConfig.type === 'doughnut') { datasets.push({ label: chartConfig.title, data: values, backgroundColor: chartConfig.colors || ['#f6ad55', '#4299e1', '#d69e2e', '#9f7aea'], borderColor: '#ffffff', borderWidth: 1 }); } activeCharts[chartConfig.id] = new Chart(ctx, { type: chartConfig.type, data: { labels: labels, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', }, title: { display: true, text: chartConfig.title }, tooltip: { callbacks: { label: function(context) { let label = context.label || ''; if (label) { label += ': '; } if (context.parsed !== null) { label += context.parsed.toLocaleString('en-US'); if (chartConfig.type === 'pie' || chartConfig.type === 'doughnut') { const total = context.dataset.data.reduce((sum, val) => sum + val, 0); const percentage = (context.parsed / total * 100).toFixed(1); label += ` (${percentage}%)`; } } return label; } } } } } }); } }); // Render Tables tablesContainer.innerHTML = ''; currentViewData.tables.forEach(tableConfig => { const tableDiv = document.createElement('div'); tableDiv.className = 'bg-gray-50 p-4 rounded-lg shadow-sm'; let tableHtml = `

${tableConfig.title}

${tableConfig.headers.map(header => ``).join('')} ${tableConfig.data.map((row, rowIndex) => ` ${Object.values(row).map((cellValue, colIndex) => { // Create a unique ID for each editable cell for direct updates const cellId = `dash-${tableConfig.id}-row${rowIndex}-col${colIndex}`; return ``; }).join('')} `).join('')}
${header}
${cellValue.toLocaleString('en-US')}
`; tableDiv.innerHTML = tableHtml; tablesContainer.appendChild(tableDiv); }); // Attach blur listeners for direct editing on dashboard attachDashboardEditableListeners(); } /** * Attaches blur event listeners to all editable cells on the dashboard * to update the underlying dashboardData. */ function attachDashboardEditableListeners() { dashboardContentForPdf.querySelectorAll('.editable-cell').forEach(cell => { cell.removeEventListener('blur', updateDataFromDashboardEdit); // Remove old listeners to prevent duplicates cell.addEventListener('blur', updateDataFromDashboardEdit); }); } /** * Updates the dashboardData object when an editable cell on the dashboard is blurred. * Then re-renders the config tab to reflect these changes. */ function updateDataFromDashboardEdit(event) { const target = event.target; const id = target.id; let value = target.innerText.replace(/[^0-9.]/g, ''); // Clean value for numbers const currentViewData = dashboardData.views[dashboardData.currentView]; // Update Summary Metrics const summaryMetric = currentViewData.summaryMetrics.find(m => `dash-${m.id}` === id); if (summaryMetric) { summaryMetric.value = summaryMetric.format === 'currency' ? parseFloat(value) || 0 : parseInt(value) || 0; } // Update Table Data else if (id.startsWith('dash-') && id.includes('-row') && id.includes('-col')) { const parts = id.split('-'); // e.g., ['dash', 'salesDetailsTable', 'row0', 'col0'] const tableId = parts[1]; const rowIndex = parseInt(parts[2].replace('row', '')); const colIndex = parseInt(parts[3].replace('col', '')); const tableConfig = currentViewData.tables.find(t => t.id === tableId); if (tableConfig && tableConfig.data[rowIndex]) { const headerKey = Object.keys(tableConfig.data[rowIndex])[colIndex]; if (headerKey) { // Attempt to parse as number if it looks like one, otherwise keep as string tableConfig.data[rowIndex][headerKey] = isNaN(parseFloat(value)) ? target.innerText : parseFloat(value); } } } // Update Chart Data (direct editing of chart values is not typical for Chart.js, // but if the user edits a label that corresponds to chart data, we should update it) // This would require more complex ID parsing or a different approach for chart data editing. // For now, chart data is primarily updated via the config tab. // Re-render config to reflect changes if config tab is open if (currentTab === 'dataConfig') { renderConfig(); } // Re-render dashboard to ensure consistency and update calculated values renderDashboard(); } /** * Renders the data configuration input fields based on dashboardData. */ function renderConfig() { const currentViewData = dashboardData.views[configViewSelector.value]; if (!currentViewData) { console.error(`Configuration data for view "${configViewSelector.value}" not found.`); return; } configContentContainer.innerHTML = ''; // Clear existing config fields // Config for Summary Metrics let summaryConfigHtml = `

Summary Metrics

`; currentViewData.summaryMetrics.forEach((metric, index) => { summaryConfigHtml += `
`; }); summaryConfigHtml += `
`; configContentContainer.innerHTML += summaryConfigHtml; // Config for Charts currentViewData.charts.forEach((chartConfig, chartIndex) => { let chartConfigHtml = `

Chart: ${chartConfig.title} (Type: ${chartConfig.type})

`; chartConfig.data.forEach((dataPoint, dataIndex) => { chartConfigHtml += `
`; }); chartConfigHtml += `
`; configContentContainer.innerHTML += chartConfigHtml; }); // Config for Tables currentViewData.tables.forEach((tableConfig, tableIndex) => { let tableConfigHtml = `

Table: ${tableConfig.title}

`; tableConfig.data.forEach((row, rowIndex) => { tableConfigHtml += `
`; Object.entries(row).forEach(([key, value], colIndex) => { tableConfigHtml += `
`; }); tableConfigHtml += `
`; }); tableConfigHtml += `
`; configContentContainer.innerHTML += tableConfigHtml; }); } /** * Adds a new data point to a chart. * @param {number} chartIndex - The index of the chart in the current view's charts array. */ window.addChartDataPoint = function(chartIndex) { const currentViewData = dashboardData.views[configViewSelector.value]; if (currentViewData && currentViewData.charts[chartIndex]) { currentViewData.charts[chartIndex].data.push({ label: 'New Point', value: 0 }); renderConfig(); } }; /** * Removes a data point from a chart. * @param {number} chartIndex - The index of the chart. * @param {number} dataIndex - The index of the data point to remove. */ window.removeChartDataPoint = function(chartIndex, dataIndex) { const currentViewData = dashboardData.views[configViewSelector.value]; if (currentViewData && currentViewData.charts[chartIndex] && currentViewData.charts[chartIndex].data.length > 1) { currentViewData.charts[chartIndex].data.splice(dataIndex, 1); renderConfig(); } else { console.warn("Cannot remove the last data point from chart."); } }; /** * Adds a new row to a table. * @param {number} tableIndex - The index of the table in the current view's tables array. */ window.addTableRow = function(tableIndex) { const currentViewData = dashboardData.views[configViewSelector.value]; if (currentViewData && currentViewData.tables[tableIndex]) { const newRow = {}; currentViewData.tables[tableIndex].headers.forEach(header => { newRow[header.toLowerCase().replace(/\s/g, '')] = ''; // Default empty string }); currentViewData.tables[tableIndex].data.push(newRow); renderConfig(); } }; /** * Removes a row from a table. * @param {number} tableIndex - The index of the table. * @param {number} rowIndex - The index of the row to remove. */ window.removeTableRow = function(tableIndex, rowIndex) { const currentViewData = dashboardData.views[configViewSelector.value]; if (currentViewData && currentViewData.tables[tableIndex] && currentViewData.tables[tableIndex].data.length > 1) { currentViewData.tables[tableIndex].data.splice(rowIndex, 1); renderConfig(); } else { console.warn("Cannot remove the last row from table."); } }; /** * Applies the values from the Data Configuration tab to the dashboardData object * and re-renders the dashboard. */ window.applyConfiguration = function() { const currentViewKey = configViewSelector.value; const currentViewData = dashboardData.views[currentViewKey]; // Update Summary Metrics currentViewData.summaryMetrics.forEach(metric => { const input = document.getElementById(`configSummary${metric.id}`); if (input) { metric.value = metric.format === 'currency' ? parseFloat(input.value) || 0 : parseInt(input.value) || 0; } }); // Update Chart Data currentViewData.charts.forEach((chartConfig, chartIndex) => { const newChartData = []; document.querySelectorAll(`#configChartData${chartIndex} .flex.flex-col.md\\:flex-row.gap-2.items-end`).forEach((div, dataIndex) => { const labelInput = div.querySelector(`#configChart${chartIndex}Label${dataIndex}`); const valueInput = div.querySelector(`#configChart${chartIndex}Value${dataIndex}`); if (labelInput && valueInput) { newChartData.push({ label: labelInput.value, value: parseFloat(valueInput.value) || 0 }); } }); chartConfig.data = newChartData; }); // Update Table Data currentViewData.tables.forEach((tableConfig, tableIndex) => { const newTableData = []; document.querySelectorAll(`#configTableData${tableIndex} .flex.flex-col.md\\:flex-row.gap-2.items-end`).forEach((div, rowIndex) => { const newRow = {}; tableConfig.headers.forEach((header, colIndex) => { const input = div.querySelector(`#configTable${tableIndex}Row${rowIndex}Col${colIndex}`); if (input) { newRow[header.toLowerCase().replace(/\s/g, '')] = isNaN(parseFloat(input.value)) ? input.value : parseFloat(input.value); } }); newTableData.push(newRow); }); tableConfig.data = newTableData; }); // Switch to the updated view on the dashboard dashboardData.currentView = currentViewKey; dataViewSelector.value = currentViewKey; // Ensure dashboard selector is updated renderDashboard(); openTab('dashboard'); // Switch back to dashboard }; /** * Handles the PDF download functionality. * Captures the 'dashboard-content-for-pdf' div and generates a PDF. */ window.downloadPdf = function() { const dashboardContent = document.getElementById('dashboard-content-for-pdf'); if (!dashboardContent) { console.error("Dashboard content for PDF not found."); return; } // Temporarily hide elements not needed in PDF (e.g., contenteditable borders) const editableCells = dashboardContent.querySelectorAll('.editable-cell'); editableCells.forEach(cell => { cell.style.border = 'none'; // Remove borders for PDF cell.style.backgroundColor = 'transparent'; // Remove background for PDF }); // Use html2canvas to capture the dashboard content html2canvas(dashboardContent, { scale: 2, // Increase scale for better resolution in PDF useCORS: true, // Required if images are from external sources (though not used here) logging: false // Disable logging for cleaner console }).then(canvas => { // Create a new jsPDF instance const { jsPDF } = window.jspdf; const pdf = new jsPDF('p', 'mm', 'a4'); // 'p' for portrait, 'mm' for millimeters, 'a4' size const imgData = canvas.toDataURL('image/png'); const imgWidth = 190; // A4 width in mm minus margins const pageHeight = 297; // A4 height in mm const imgHeight = (canvas.height * imgWidth) / canvas.width; const xOffset = (pdf.internal.pageSize.getWidth() - imgWidth) / 2; // Center image let yPos = 10; pdf.setFontSize(22); pdf.text("Dynamic Data Dashboard", 105, yPos, { align: 'center' }); yPos += 15; // Add image of the dashboard to PDF pdf.addImage(imgData, 'PNG', xOffset, yPos, imgWidth, imgHeight); yPos += imgHeight + 10; // Add structured data tables to PDF for clarity const currentViewData = dashboardData.views[dashboardData.currentView]; if (yPos > 250) { // Check if enough space for next section pdf.addPage(); yPos = 10; } pdf.setFontSize(14); pdf.text(`Detailed Data for ${currentViewData.title}:`, 10, yPos); yPos += 10; // Add Summary Metrics Table if (currentViewData.summaryMetrics.length > 0) { if (yPos > 250) { pdf.addPage(); yPos = 10; } pdf.setFontSize(12); pdf.text("Summary Metrics:", 10, yPos); yPos += 5; pdf.autoTable({ head: [currentViewData.summaryMetrics.map(m => m.label)], body: [currentViewData.summaryMetrics.map(m => { return m.format === 'currency' ? `$${m.value.toLocaleString('en-US')}` : m.value.toLocaleString('en-US'); })], startY: yPos, theme: 'grid', headStyles: { fillColor: [66, 153, 225], textColor: [255, 255, 255], fontStyle: 'bold' }, styles: { fontSize: 9, cellPadding: 3, textColor: [74, 85, 104] } }); yPos = pdf.autoTable.previous.finalY + 10; } // Add Chart Data Tables (as tabular data) currentViewData.charts.forEach(chartConfig => { if (chartConfig.data.length > 0) { if (yPos > 250) { pdf.addPage(); yPos = 10; } pdf.setFontSize(12); pdf.text(`Chart Data: ${chartConfig.title}`, 10, yPos); yPos += 5; pdf.autoTable({ head: [['Label', 'Value']], body: chartConfig.data.map(d => [d.label, d.value.toLocaleString('en-US')]), startY: yPos, theme: 'grid', headStyles: { fillColor: [72, 187, 120], textColor: [255, 255, 255], fontStyle: 'bold' }, styles: { fontSize: 9, cellPadding: 3, textColor: [74, 85, 104] } }); yPos = pdf.autoTable.previous.finalY + 10; } }); // Add Tables Data currentViewData.tables.forEach(tableConfig => { if (tableConfig.data.length > 0) { if (yPos > 250) { pdf.addPage(); yPos = 10; } pdf.setFontSize(12); pdf.text(`Table Data: ${tableConfig.title}`, 10, yPos); yPos += 5; pdf.autoTable({ head: [tableConfig.headers], body: tableConfig.data.map(row => Object.values(row).map(val => val.toLocaleString('en-US'))), startY: yPos, theme: 'grid', headStyles: { fillColor: [128, 0, 128], textColor: [255, 255, 255], fontStyle: 'bold' }, styles: { fontSize: 9, cellPadding: 3, textColor: [74, 85, 104] }, didDrawPage: function (data) { let str = "Page " + pdf.internal.getNumberOfPages(); pdf.setFontSize(10); pdf.text(str, data.settings.margin.left, pdf.internal.pageSize.height - 10); } }); yPos = pdf.autoTable.previous.finalY + 10; } }); pdf.save('Dynamic_Data_Dashboard.pdf'); // Restore original styles for editable cells after PDF generation editableCells.forEach(cell => { cell.style.border = ''; // Restore default border cell.style.backgroundColor = ''; // Restore default background }); }).catch(error => { console.error("Error generating PDF:", error); }); }; // Attach event listeners downloadPdfBtn.addEventListener('click', downloadPdf); applyConfigBtn.addEventListener('click', applyConfiguration); // Initial rendering and setup renderDashboard(); renderConfig(); // Render config metrics initially to populate inputs updateNavigationButtons(); // Set initial button visibility });
Scroll to Top