// ============================================ // Helpers — Utility functions // ============================================ const Helpers = { // DOM selection helpers $(selector, context = document) { return context.querySelector(selector); }, $$(selector, context = document) { return Array.from(context.querySelectorAll(selector)); }, // Deep clone utility deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }, // Debounce function debounce(fn, delay = 300) { let timer = null; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; }, // Format file size formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; }, // Format date formatDate(dateStr) { const d = new Date(dateStr); return d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); }, // Get category color getCategoryColor(categoryId) { const colors = { 'good': '#22c55e', 'd02': '#ef4444', 'd07': '#ef4444', 'd08': '#f59e0b', 'unsorted': '#3b82f6' }; return colors[categoryId] || '#818891'; }, // Get category icon getCategoryIcon(categoryId) { const icons = { 'good': 'fa-check-circle', 'd02': 'fa-wind', 'd07': 'fa-exclamation-triangle', 'd08': 'fa-cut', 'unsorted': 'fa-inbox' }; return icons[categoryId] || 'fa-image'; }, // Get category label getCategoryLabel(categoryId) { const labels = { 'good': 'Good', 'd02': 'D02 — Fibres > 0.5mm', 'd07': 'D07 — Critical Defects', 'd08': 'D08 — Critical Trimming', 'unsorted': 'Unsorted' }; return labels[categoryId] || categoryId; }, // Get image preview URL — uses sample set images (228 real images) getImagePreviewUrl(imageId) { // Try to get the image from mock data const image = MockData.images.find(img => img.id === imageId); if (image) { return this._getImagePathByFilename(image.filename, image.category); } // Fallback: direct numeric key const numId = parseInt(imageId.replace('img-', '')); if (!isNaN(numId) && numId >= 1 && numId <= 228) { return 'assets/images-thumb/' + numId + '.png'; } return null; }, // Get image path by filename and category _getImagePathByFilename(filename, category) { // Map categories to directory names const categoryMap = { 'good': 'Good', 'd02': 'D02', 'd07': 'D07', 'd08': 'D08' }; const dir = categoryMap[category] || 'Good'; const thumbPath = `assets/images-thumb/${dir}/${filename.replace('.tif', '.png').replace('.tiff', '.png')}`; return thumbPath; }, // Get sample image thumbnail path by filename getSampleImageByFilename(filename) { return this._getImagePathByFilename(filename, null); }, // Generate SVG placeholder for images — with sample image fallback generatePlaceholderSVG(image, useSample = true) { if (!image) return ''; const category = image.category || 'unsorted'; const color = this.getCategoryColor(category); const label = this.getCategoryLabel(category); // If we have a sample image filename, return it instead of SVG if (useSample && image.filename) { const samplePath = this.getSampleImageByFilename(image.filename); if (samplePath) return samplePath; } const symbol = category === 'good' ? '\u2713' : (category === 'unsorted' ? '\u{1F4E2}' : '\u26A0'); const shortName = image.filename ? image.filename.replace('.tif','').replace('.tiff','').substring(0, 20) : 'Unknown'; const svg = ` ${symbol} ${label} ${shortName}... `; return 'data:image/svg+xml,' + encodeURIComponent(svg); }, // Sanitize HTML sanitize(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }, // Get current user getCurrentUser() { const userId = sessionStorage.getItem('masic_user_id'); if (userId) return MockData.getUserById(userId); return null; }, // Save current user setCurrentUser(userId) { sessionStorage.setItem('masic_user_id', userId); }, // Clear current user clearCurrentUser() { sessionStorage.removeItem('masic_user_id'); }, // Check if user is logged in isLoggedIn() { return !!sessionStorage.getItem('masic_user_id'); }, // Filter images by customer getImagesForCustomer(customerId) { return MockData.images.filter(img => img.customer === customerId); }, // Get category counts for images getCategoryCounts(images) { const counts = {}; MockData.categories.forEach(cat => { counts[cat.id] = 0; }); images.forEach(img => { if (counts[img.category] !== undefined) counts[img.category]++; }); return counts; }, // Get images by category getImagesByCategory(images, categoryId) { return images.filter(img => img.category === categoryId); }, // Count images by camera in tree countImagesByCamera(nodeId) { if (!nodeId) return 0; const node = MockData.findNodeInHierarchy(nodeId); if (!node || !node.name) return 0; // Extract customer prefix from nodeId const parts = nodeId.split('-'); let customerFilter = null; if (parts[0] === 'west') customerFilter = 'west'; if (parts[0] === 'pharma') customerFilter = 'pharma'; if (!customerFilter) return 0; if (node.type === 'camera') { return MockData.images.filter(img => img.camera === node.name && img.customer === customerFilter ).length; } // For parent nodes, sum all children let count = 0; const children = MockData.getImagesForCustomer(customerFilter); return children.length; }, // Build breadcrumb string from node path buildBreadcrumb(nodeId) { if (!nodeId) return ''; const parts = nodeId.split('-'); let breadcrumb = ''; if (parts[0] === 'west') { breadcrumb = 'West Pharmaceutical'; if (parts.length > 2) breadcrumb += ' / Site A'; if (parts.length > 3) breadcrumb += ' / ' + parts[2].toUpperCase(); if (parts.length > 4) breadcrumb += ' / ' + parts.slice(2).join(' ').toUpperCase(); } else if (parts[0] === 'pharma') { breadcrumb = 'PharmaPack BV'; if (parts.length > 2) breadcrumb += ' / Site A'; if (parts.length > 3) breadcrumb += ' / ' + parts[2].toUpperCase(); if (parts.length > 4) breadcrumb += ' / ' + parts.slice(2).join(' ').toUpperCase(); } return breadcrumb; }, // Draw ROC curve on canvas drawROCCanvas(canvasId, modelData) { const canvas = document.getElementById(canvasId); if (!canvas) return; const ctx = canvas.getContext('2d'); const width = canvas.width; const height = canvas.height; const padding = 40; // Clear canvas ctx.clearRect(0, 0, width, height); // Draw axes ctx.strokeStyle = '#e2e5e8'; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(padding, padding); ctx.lineTo(padding, height - padding); ctx.lineTo(width - padding, height - padding); ctx.stroke(); // Draw diagonal (random classifier) ctx.strokeStyle = '#e2e5e8'; ctx.lineWidth = 1; ctx.setLineDash([5, 5]); ctx.beginPath(); ctx.moveTo(padding, height - padding); ctx.lineTo(width - padding, padding); ctx.stroke(); ctx.setLineDash([]); // Axis labels ctx.fillStyle = '#818891'; ctx.font = '11px Open Sans, sans-serif'; ctx.textAlign = 'center'; ctx.fillText('False Positive Rate', padding + (width - 2 * padding) / 2, height - 10); ctx.save(); ctx.translate(12, padding + (height - 2 * padding) / 2); ctx.rotate(-Math.PI / 2); ctx.fillText('True Positive Rate', 0, 0); ctx.restore(); // Draw ROC curve if (modelData && modelData.rocData && modelData.rocData.length > 0) { const rocData = modelData.rocData; const color = modelData.color || '#a7002b'; const drawWidth = width - 2 * padding; const drawHeight = height - 2 * padding; // Calculate AUC for display let auc = 0; for (let i = 1; i < rocData.length; i++) { auc += (rocData[i].fpr - rocData[i - 1].fpr) * (rocData[i].tpr + rocData[i - 1].tpr) / 2; } ctx.strokeStyle = color; ctx.lineWidth = 2; ctx.beginPath(); rocData.forEach((point, i) => { const x = padding + (point.fpr * drawWidth); const y = height - padding - (point.tpr * drawHeight); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); // Fill area under curve ctx.lineTo(padding + drawWidth, height - padding); ctx.lineTo(padding, height - padding); ctx.closePath(); ctx.fillStyle = color + '15'; ctx.fill(); // Draw AUC label ctx.fillStyle = color; ctx.font = 'bold 14px Open Sans, sans-serif'; ctx.textAlign = 'left'; ctx.fillText(`AUC = ${auc.toFixed(3)}`, padding + 10, padding - 10); } }, // Get category badge HTML getCategoryBadge(categoryId) { const colors = { 'good': '#22c55e', 'd02': '#ef4444', 'd07': '#ef4444', 'd08': '#f59e0b', 'unsorted': '#3b82f6' }; const color = colors[categoryId] || '#818891'; const label = this.getCategoryLabel(categoryId); return `${label}`; }, // Get reason badge HTML getReasonBadge(reason) { const reasons = { 'corruption': 'Corrupted File', 'metadata': 'Metadata Issue', 'security': 'Security Flag', 'duplicate': 'Duplicate File' }; const colors = { 'corruption': '#ef4444', 'metadata': '#f59e0b', 'security': '#a7002b', 'duplicate': '#3b82f6' }; const color = colors[reason] || '#818891'; const label = reasons[reason] || reason; return `${label}`; }, // Get status badge HTML getStatusBadge(status) { const states = { 'healthy': 'healthy', 'warning': 'warning', 'error': 'error', 'starting': 'starting' }; const state = states[status] || 'healthy'; const labels = { 'healthy': 'Healthy', 'warning': 'Warning', 'error': 'Error', 'starting': 'Starting...' }; return `${labels[state]}`; }, // Create metric card HTML createMetricCard(label, value, highIsBetter = true) { const numVal = parseFloat(value); let valueClass = ''; if (highIsBetter) { if (numVal >= 0.95) valueClass = 'metric-card__value--high'; else if (numVal >= 0.90) valueClass = 'metric-card__value--mid'; else valueClass = 'metric-card__value--low'; } else { if (numVal <= 0.02) valueClass = 'metric-card__value--high'; else if (numVal <= 0.05) valueClass = 'metric-card__value--mid'; else valueClass = 'metric-card__value--low'; } return `
${Helpers.sanitize(label)}
${(value * 100).toFixed(1)}%
`; } };