${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 => `${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 `${cellValue.toLocaleString('en-US')} `;
}).join('')}
`).join('')}
`;
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 = `
`;
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 += `
Add Data Point
`;
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 += `
${key}:
`;
});
tableConfigHtml += `
Remove Row
`;
});
tableConfigHtml += `
Add New Row
`;
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
});