MonitorProgress/static/js/chartManager.js

274 lines
13 KiB
JavaScript

// --- Chart Management ---
// Keep chart instances internal to this module
let chartInstancesScada = {};
let chartInstancesDrawing = {};
// --- Chart Configurations ---
const scadaChartLabels = ['Found in SCADA', 'Not Found in SCADA'];
// const scadaChartColors = ['rgb(13, 110, 253)', 'rgb(220, 53, 69)']; // Removed
const drawingChartLabels = ['Found in Drawing', 'Not Found in Drawing'];
// const drawingChartColors = ['rgb(25, 135, 84)', 'rgb(220, 53, 69)']; // Removed
// Unified colors: Green for Found, Red for Not Found
const commonChartColors = ['rgb(25, 135, 84)', 'rgb(220, 53, 69)'];
// --- Chart Click Handler ---
// Note: This needs access to showDetailsModal, which might need to be passed or refactored.
function handleChartClick(event, elements, chart, context) {
if (elements.length > 0 && selectedProjectName) { // Relies on global selectedProjectName for now
const clickedElementIndex = elements[0].index;
const isOverallChart = chart.canvas.id.startsWith('overall-');
const identifier = isOverallChart ? '__overall__' : chart.canvas.id.replace(`chart-${context}-`, '');
const categoryType = clickedElementIndex === 0 ? 'found' : 'notFound';
// TODO: Refactor showDetailsModal call - currently global
// showDetailsModal(selectedProjectName, identifier, categoryType, context);
// Call modalManager function instead
modalManagerShowDetails(selectedProjectName, identifier, categoryType, context);
}
}
// --- Generic Helper to create chart config ---
// Note: Relies on global currentProjectData and selectedProjectName for tooltips
function createChartConfig(chartCounts, total, context, identifier, projectName) {
const labels = context === 'scada' ? scadaChartLabels : drawingChartLabels;
// const colors = context === 'scada' ? scadaChartColors : drawingChartColors; // Use common colors instead
const datasetLabel = context === 'scada' ? 'SCADA Match' : 'Drawing Match';
// Retrieve the correct project's progress data for tooltip calculation
// TODO: Find a way to access state without global reliance (e.g., pass data in)
const projectProgress = (currentProjectData[projectName] && currentProjectData[projectName].progress) ? currentProjectData[projectName].progress : {};
return {
type: 'pie',
data: {
labels: labels,
datasets: [{
label: datasetLabel,
data: chartCounts,
backgroundColor: commonChartColors, // Use unified colors
hoverOffset: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
onClick: (event, elements, chart) => handleChartClick(event, elements, chart, context), // Pass context
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(ctxTooltip) {
let label = ctxTooltip.label || '';
if (label) label += ': ';
const value = ctxTooltip.parsed;
if (value !== null) label += value;
// Use total passed for panel charts, access stored data for overall
const chartTotal = (identifier === 'overall' && projectProgress.overall)
? projectProgress.overall.total_csv
: total;
if (chartTotal && chartTotal > 0 && value !== null) {
label += ` (${((value / chartTotal) * 100).toFixed(1)}%)`;
}
return label;
}
}
}
}
}
};
}
// --- Update Functions ---
/**
* Updates or creates the overall progress chart for a context (scada/drawing).
* @param {string} context - 'scada' or 'drawing'
* @param {string} canvasId - The ID of the canvas element.
* @param {Array<number>} chartCounts - The data array for the chart (e.g., [found, notFound]).
* @param {number} total - The total number of items for percentage calculation.
* @param {boolean} isVisible - Whether the section containing the chart is currently visible.
*/
function updateOverallChart(context, canvasId, chartCounts, total, isVisible) {
console.log(`[ChartManager] updateOverallChart called for context: ${context}, canvasId: ${canvasId}`); // Log entry
console.log(`[ChartManager] Received chartCounts: ${JSON.stringify(chartCounts)}, total: ${total}`); // Log data
const chartInstances = context === 'scada' ? chartInstancesScada : chartInstancesDrawing;
const chartKey = 'overall';
// Find the parent container based on context
const parentContainer = context === 'scada' ? uiElements.overallScadaProgress : uiElements.overallDrawingProgress;
console.log(`[ChartManager] Parent container for ${context}:`, parentContainer); // Log container found
if (!parentContainer) {
console.error(`[ChartManager] Parent container for overall ${context} chart not found.`);
return;
}
let canvas = document.getElementById(canvasId);
console.log(`[ChartManager] Existing canvas found for ${canvasId}:`, canvas);
// Check if the found canvas is unusable (zero dimensions)
if (canvas && (canvas.offsetWidth === 0 || canvas.offsetHeight === 0)) {
console.warn(`[ChartManager] Found canvas ${canvasId} has zero dimensions. Removing and recreating.`);
// Destroy the associated Chart.js instance if it exists
if (chartInstances[chartKey]) {
console.log(`[ChartManager] Destroying Chart.js instance for zero-dimension canvas ${canvasId}.`);
chartInstances[chartKey].destroy();
delete chartInstances[chartKey];
}
canvas.remove(); // Remove the faulty canvas element
canvas = null; // Force recreation below
}
// If canvas doesn't exist (or was removed), create and append it
if (!canvas) {
console.log(`[ChartManager] Canvas ${canvasId} needs creation.`);
canvas = document.createElement('canvas');
canvas.id = canvasId;
canvas.className = 'overall-chart-canvas';
parentContainer.innerHTML = ''; // Clear the container first before adding canvas
parentContainer.appendChild(canvas);
console.log(`[ChartManager] Canvas ${canvasId} created and appended.`);
}
// Now canvas definitely exists (or we returned early)
if (chartInstances[chartKey]) {
// Update existing chart data if it changed
if (JSON.stringify(chartInstances[chartKey].data.datasets[0].data) !== JSON.stringify(chartCounts)) {
console.log(`Updating overall ${context} chart data.`);
chartInstances[chartKey].data.datasets[0].data = chartCounts;
chartInstances[chartKey].update('none'); // Update without animation
}
} else { // No existing chart instance, create new one
// Create new chart
console.log(`Creating overall ${context} chart.`);
const ctx = canvas.getContext('2d');
if (ctx) {
chartInstances[chartKey] = new Chart(ctx, createChartConfig(chartCounts, total, context, chartKey, selectedProjectName)); // Relies on global selectedProjectName
} else {
console.error(`[ChartManager] Failed to get 2D context for canvas ${canvasId}.`);
}
}
}
/**
* Updates or creates/destroys panel-specific charts for a context.
* @param {string} context - 'scada' or 'drawing'
* @param {HTMLElement} panelsContainer - The container element for panel charts.
* @param {object} panelsData - The progress data object for all panels.
* @param {boolean} isVisible - Whether the section containing the charts is currently visible.
*/
function updatePanelCharts(context, panelsContainer, panelsData, isVisible) {
const chartInstances = context === 'scada' ? chartInstancesScada : chartInstancesDrawing;
const incomingPanelNames = new Set(Object.keys(panelsData || {}).sort());
const existingInstanceNames = new Set(Object.keys(chartInstances).filter(k => k !== 'overall'));
if (!isVisible) {
// If section is not visible, destroy all existing panel charts for this context
console.log(`[ChartManager] Destroying hidden panel charts for context: ${context}`);
existingInstanceNames.forEach(panelName => {
if (chartInstances[panelName]) {
chartInstances[panelName].destroy();
delete chartInstances[panelName];
}
});
// Remove any dynamically created chart containers if section is hidden
panelsContainer.querySelectorAll(`.chart-container[id^="chart-container-${context}-"]`).forEach(el => el.remove());
if (!panelsContainer.querySelector('p')) { // Add placeholder if empty
panelsContainer.innerHTML = '<p class="text-center fst-italic">Panel data hidden.</p>';
}
return; // Don't proceed further if the section is hidden
}
// Clear placeholder/hidden message if we have data and are visible
const placeholder = panelsContainer.querySelector('p');
if(placeholder) placeholder.remove();
// Update/Create charts for incoming panels
if (incomingPanelNames.size > 0) {
incomingPanelNames.forEach(panelName => {
const panel = panelsData[panelName];
const panelTotal = (panel && panel.total) || 0;
let panelChartCounts = [0, 0];
if (panel) {
if (context === 'scada') {
panelChartCounts = [(panel.found_both || 0) + (panel.found_scada_only || 0), (panel.found_drawing_only || 0) + (panel.missing_both || 0)];
} else { // drawing
panelChartCounts = [(panel.found_both || 0) + (panel.found_drawing_only || 0), (panel.found_scada_only || 0) + (panel.missing_both || 0)];
}
}
const chartKey = panelName;
const canvasId = `chart-${context}-${panelName}`;
const containerId = `chart-container-${context}-${panelName}`;
if (chartInstances[chartKey]) {
// Update existing chart
if (JSON.stringify(chartInstances[chartKey].data.datasets[0].data) !== JSON.stringify(panelChartCounts)) {
chartInstances[chartKey].data.datasets[0].data = panelChartCounts;
// Update tooltip data source if needed (or ensure createChartConfig handles it)
chartInstances[chartKey].update('none');
}
} else {
// Create new chart element and instance
let container = document.getElementById(containerId);
if (!container) {
console.log(`Creating new ${context} panel elements and chart (visible) for: ${panelName}`);
container = document.createElement('div');
container.id = containerId;
container.className = 'chart-container';
container.innerHTML = `
<span class="chart-label">${panelName}</span>
<canvas id="${canvasId}" class="panel-chart-canvas"></canvas>
`;
panelsContainer.appendChild(container);
}
const canvas = document.getElementById(canvasId);
if (canvas) {
const ctx = canvas.getContext('2d');
chartInstances[chartKey] = new Chart(ctx, createChartConfig(panelChartCounts, panelTotal, context, chartKey, selectedProjectName)); // Relies on global selectedProjectName
} else {
console.error(`Canvas element with ID ${canvasId} not found after creation.`);
}
}
});
} else {
// No panel data, ensure placeholder is shown
if (!panelsContainer.querySelector('p')) {
panelsContainer.innerHTML = '<p class="text-center fst-italic">No panel data available yet.</p>';
}
}
// Remove charts and elements for panels that no longer exist
existingInstanceNames.forEach(panelName => {
if (!incomingPanelNames.has(panelName)) {
console.log(`Removing ${context} panel elements and chart for: ${panelName}`);
if (chartInstances[panelName]) {
chartInstances[panelName].destroy();
delete chartInstances[panelName];
}
const chartElement = document.getElementById(`chart-container-${context}-${panelName}`);
if (chartElement) {
chartElement.remove();
}
}
});
}
/**
* Destroys all tracked chart instances.
*/
function chartManagerDestroyAll() {
console.log("[ChartManager] Destroying all chart instances.");
Object.values(chartInstancesScada).forEach(chart => chart?.destroy());
chartInstancesScada = {};
Object.values(chartInstancesDrawing).forEach(chart => chart?.destroy());
chartInstancesDrawing = {};
}
// --- Export (if using modules in the future) ---
// export { updateOverallChart, updatePanelCharts, chartManagerDestroyAll };