/** * CVE Forecast Dashboard JavaScript * Handles data loading, visualization, and user interactions. */ // Theme toggle with localStorage persistence (function initTheme() { const toggle = document.getElementById('themeToggle'); const sunIcon = document.getElementById('themeSun'); const moonIcon = document.getElementById('themeMoon'); function applyTheme(theme) { if (theme === 'dark') { document.documentElement.setAttribute('data-theme', 'dark'); if (sunIcon) sunIcon.classList.remove('hidden'); if (moonIcon) moonIcon.classList.add('hidden'); } else { document.documentElement.setAttribute('data-theme', 'light'); if (sunIcon) sunIcon.classList.add('hidden'); if (moonIcon) moonIcon.classList.remove('hidden'); } } // Check saved preference, then system preference const saved = localStorage.getItem('theme'); if (saved) { applyTheme(saved); } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) { applyTheme('dark'); } if (toggle) { toggle.addEventListener('click', function() { const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; const newTheme = isDark ? 'light' : 'dark'; applyTheme(newTheme); localStorage.setItem('theme', newTheme); }); } })(); // Constants const TOP_MODELS_COUNT = 5; const CHART_ANIMATION_DURATION = 750; const CACHE_BUSTER = new Date().getTime(); // Global state variables let forecastData = null; let modelInfoData = null; let chartInstance = null; let selectedYear = new Date().getFullYear(); // Default to current year // Initialize the dashboard when the DOM is loaded document.addEventListener('DOMContentLoaded', loadForecastData); /** * Loads forecast data from the data.json file and initializes the dashboard. */ async function loadForecastData() { console.log('🔄 Loading application data...'); try { const [forecastResponse, modelInfoResponse] = await Promise.all([ fetch('data.json?v=' + CACHE_BUSTER), // Cache-busting for forecast data fetch('model_info.json?v=' + CACHE_BUSTER) // Cache-busting for model info ]); if (!forecastResponse.ok) { throw new Error(`HTTP error! Status: ${forecastResponse.status} on data.json`); } if (!modelInfoResponse.ok) { console.warn(`Could not load model_info.json. Status: ${modelInfoResponse.status}. Links will not be available.`); modelInfoData = {}; // Set to empty object to prevent errors } else { try { modelInfoData = await modelInfoResponse.json(); } catch (e) { console.error('Error parsing model_info.json:', e); modelInfoData = {}; } } try { forecastData = await forecastResponse.json(); } catch (e) { console.error('Error parsing data.json:', e); throw new Error('Failed to parse data.json'); } console.log('✅ Application data loaded successfully.'); document.getElementById('loadingState').classList.add('hidden'); document.getElementById('dashboard').classList.remove('hidden'); initializeDashboard(); console.log('✅ Dashboard initialized successfully!'); } catch (error) { console.error('❌ Error in loadForecastData:', error); if (error instanceof Error) { console.error('Error message:', error.message); console.error('Error stack:', error.stack); } document.getElementById('loadingState').classList.add('hidden'); document.getElementById('errorState').classList.remove('hidden'); } } /** * Initializes all dashboard components with the loaded data. */ function initializeDashboard() { updateSummaryCards(); populateModelSelector(); populateModelRankings(); populateForecastVsPublishedTable(); // Initially populate with the best model updateDataPeriodInfo(); updateChartDescription(); createOrUpdateChart(); const validationModelSelector = document.getElementById('validationModelSelector'); if (validationModelSelector) { validationModelSelector.addEventListener('change', populateForecastVsPublishedTable); } } /** * Switches the chart to display a specific year. * @param {number} year - The year to display */ function switchYear(year) { selectedYear = year; // Update button styles (buttons may not exist in all versions) const currentYear = new Date().getFullYear(); const btnCurrent = document.getElementById(`yearBtn${currentYear}`); const btnNext = document.getElementById(`yearBtn${currentYear + 1}`); if (btnCurrent && btnNext) { if (year === currentYear) { btnCurrent.className = 'px-4 py-2 rounded-lg font-semibold transition-colors bg-blue-600 text-white hover:bg-blue-700'; btnNext.className = 'px-4 py-2 rounded-lg font-semibold transition-colors bg-gray-200 text-gray-700 hover:bg-gray-300'; } else { btnCurrent.className = 'px-4 py-2 rounded-lg font-semibold transition-colors bg-gray-200 text-gray-700 hover:bg-gray-300'; btnNext.className = 'px-4 py-2 rounded-lg font-semibold transition-colors bg-blue-600 text-white hover:bg-blue-700'; } } updateChartDescription(); updateSummaryCards(); // Update cards for selected year createOrUpdateChart(); } /** * Updates the chart description with the selected year. */ function updateChartDescription() { document.getElementById('chartDescription').textContent = `Cumulative growth showing actual CVE publications and ML model predictions for ${selectedYear}`; } /** * Updates the summary cards at the top of the dashboard. */ function updateSummaryCards() { if (!forecastData) return; document.getElementById('lastUpdated').textContent = `Last Updated: ${new Date(forecastData.generated_at).toLocaleString()}`; const bestModelName = forecastData.model_rankings?.[0]?.model_name || 'N/A'; const yearlyTotals = forecastData.yearly_forecast_totals || {}; const yearTotals = yearlyTotals[selectedYear] || {}; const bestModelTotal = yearTotals[bestModelName] || 0; document.getElementById('currentYearForecast').textContent = bestModelTotal.toLocaleString(); document.getElementById('forecastDescription').textContent = `Total CVEs: Published + Forecasted (${bestModelName} - Best Model)`; if (forecastData.model_rankings?.length > 0) { const bestModel = forecastData.model_rankings[0]; document.getElementById('bestModel').textContent = bestModel.model_name; document.getElementById('bestAccuracy').textContent = `${(bestModel.mape || 0).toFixed(2)}%`; } document.getElementById('totalCVEs').textContent = (forecastData.summary?.total_historical_cves || 0).toLocaleString(); // Calculate year-over-year growth for selected year const previousYear = selectedYear - 1; const previousYearTotals = yearlyTotals[previousYear] || {}; let lastYearTotal = previousYearTotals[bestModelName] || 0; // Fallback: use actual previous year total from summary if forecast not available if (!lastYearTotal) { lastYearTotal = forecastData.summary?.previous_year_total || 0; } if (bestModelTotal && lastYearTotal) { const yoyGrowth = ((bestModelTotal - lastYearTotal) / lastYearTotal) * 100; const growthText = yoyGrowth >= 0 ? `+${yoyGrowth.toFixed(1)}%` : `${yoyGrowth.toFixed(1)}%`; const yoyGrowthEl = document.getElementById('yoyGrowth'); const yoyGrowthDetailEl = document.getElementById('yoyGrowthDetail'); if (yoyGrowthEl) yoyGrowthEl.textContent = growthText; if (yoyGrowthDetailEl) yoyGrowthDetailEl.textContent = `${bestModelTotal.toLocaleString()} vs ${lastYearTotal.toLocaleString()} (${selectedYear} vs ${previousYear})`; } else { const yoyGrowthEl = document.getElementById('yoyGrowth'); const yoyGrowthDetailEl = document.getElementById('yoyGrowthDetail'); if (yoyGrowthEl) yoyGrowthEl.textContent = '-'; if (yoyGrowthDetailEl) yoyGrowthDetailEl.textContent = 'Data unavailable'; } } /** * Populates the model selector dropdown. */ function populateModelSelector() { const selector = document.getElementById('validationModelSelector'); if (!selector) return; selector.innerHTML = ''; // Get the top models from the rankings const topModels = forecastData.model_rankings?.slice(0, TOP_MODELS_COUNT) || []; topModels.forEach(model => { const option = document.createElement('option'); option.value = model.model_name; option.textContent = model.model_name; selector.appendChild(option); }); } /** * Populates the model performance rankings table. */ function populateModelRankings() { const tableBody = document.getElementById('modelRankingsTable'); if (!tableBody) return; tableBody.innerHTML = ''; forecastData.model_rankings?.forEach((model, index) => { const row = document.createElement('tr'); const mape = model.mape || 0; let badgeClass = 'bg-red-100 text-red-800'; let performanceBadge = 'Poor'; if (mape < 10) { badgeClass = 'bg-green-100 text-green-800'; performanceBadge = 'Excellent'; } else if (mape < 15) { badgeClass = 'bg-blue-100 text-blue-800'; performanceBadge = 'Good'; } else if (mape < 25) { badgeClass = 'bg-yellow-100 text-yellow-800'; performanceBadge = 'Fair'; } const modelUrl = modelInfoData ? modelInfoData[model.model_name] : null; const modelNameHtml = modelUrl ? `${model.model_name}` : model.model_name; row.innerHTML = ` ${index + 1} ${modelNameHtml} ${(model.mape || 0).toFixed(2)}% ${(model.mae || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${performanceBadge} `; tableBody.appendChild(row); // Create config row (initially hidden) const configRow = document.createElement('tr'); configRow.id = `configRow${index}`; configRow.className = 'config-row hidden-row'; configRow.innerHTML = `

Configuration for ${model.model_name}

`; tableBody.appendChild(configRow); // Set the configuration content after the element is in the DOM const configDisplay = document.getElementById(`configDisplay${index}`); if (configDisplay) { configDisplay.innerHTML = formatModelConfig(model); } }); } /** * Populates the 'Forecast vs Published' table with collapsible year sections. */ function populateForecastVsPublishedTable() { const selector = document.getElementById('validationModelSelector'); const selectedModel = selector.value; // Use correct IDs for summary cards and table body const tableBody = document.getElementById('validationTable'); const maeCard = document.getElementById('avgErrorCard'); const mapeCard = document.getElementById('avgPercentErrorCard'); // Add null checks to avoid JS errors if elements are missing if (!tableBody || !maeCard || !mapeCard) { console.error('❌ Missing validation table or summary card elements in HTML.'); return; } if (!forecastData.forecast_vs_published || !forecastData.forecast_vs_published[selectedModel]) { tableBody.innerHTML = 'No data available for this model.'; maeCard.textContent = '-'; mapeCard.textContent = '-'; return; } const modelData = forecastData.forecast_vs_published[selectedModel]; const tableData = modelData.table_data; const summaryStats = modelData.summary_stats; maeCard.textContent = (summaryStats.mean_absolute_error || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }); mapeCard.textContent = `${(summaryStats.mean_absolute_percentage_error || 0).toFixed(2)}%`; const groupedData = tableData.reduce((acc, row) => { const year = row.MONTH.split('-')[0]; if (!acc[year]) { acc[year] = []; } acc[year].push(row); return acc; }, {}); tableBody.innerHTML = ''; const currentDisplayYear = new Date().getFullYear().toString(); const sortedYears = Object.keys(groupedData).sort((a, b) => b - a); sortedYears.forEach(year => { const months = groupedData[year].sort((a, b) => new Date(b.MONTH) - new Date(a.MONTH)); const isCollapsed = year !== currentDisplayYear; const headerRow = document.createElement('tr'); headerRow.className = 'year-header'; // Add Tailwind bg-gray-100 for year header shading headerRow.innerHTML = ` ${isCollapsed ? '▶' : '▼'} ${year} `; if (isCollapsed) headerRow.classList.add('collapsed'); tableBody.appendChild(headerRow); months.forEach(row => { const dataRow = document.createElement('tr'); dataRow.className = `month-row year-${year}`; if (isCollapsed) { dataRow.classList.add('hidden-row'); } const now = new Date(); const currentMonthStr = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}`; const formatMonth = (monthStr) => { const [year, month] = monthStr.split('-'); const date = new Date(year, parseInt(month, 10) - 1); return date.toLocaleString('en-US', { month: 'long' }); }; if (row.MONTH === currentMonthStr) { dataRow.innerHTML = ` ${formatMonth(row.MONTH)} ${row.PUBLISHED.toLocaleString()} ${row.FORECAST.toLocaleString()} In Progress `; } else { const error = row.ERROR; const percentError = row.PERCENT_ERROR; const formatNumberWithSign = (num) => { const sign = num > 0 ? '+' : ''; return `${sign}${num.toLocaleString()}`; }; const formatPercentWithSign = (num) => { const sign = num > 0 ? '+' : ''; return `${sign}${num.toFixed(2)}%`; }; let badgeClass = 'bg-red-100 text-red-800'; let performanceBadge = 'Poor'; const absPercentError = Math.abs(percentError); if (absPercentError < 10) { badgeClass = 'bg-green-100 text-green-800'; performanceBadge = 'Excellent'; } else if (absPercentError < 15) { badgeClass = 'bg-blue-100 text-blue-800'; performanceBadge = 'Good'; } else if (absPercentError < 25) { badgeClass = 'bg-yellow-100 text-yellow-800'; performanceBadge = 'Fair'; } dataRow.innerHTML = ` ${formatMonth(row.MONTH)} ${row.PUBLISHED.toLocaleString()} ${row.FORECAST.toLocaleString()} ${formatNumberWithSign(error)} ${formatPercentWithSign(percentError)} ${performanceBadge} `; } tableBody.appendChild(dataRow); }); headerRow.addEventListener('click', () => { const icon = headerRow.querySelector('.toggle-icon'); const isNowCollapsed = headerRow.classList.toggle('collapsed'); icon.textContent = isNowCollapsed ? '▶' : '▼'; // Toggle visibility of all month rows for this year const yearRows = tableBody.querySelectorAll(`.year-${year}`); yearRows.forEach(row => { if (isNowCollapsed) { row.classList.add('hidden-row'); } else { row.classList.remove('hidden-row'); } }); }); }); } /** * Updates the data period information cards. */ function updateDataPeriodInfo() { const summary = forecastData.summary; if (!summary) return; const formatDate = (dateStr) => new Date(dateStr).toLocaleDateString('en-US', { year: 'numeric', month: 'long', timeZone: 'UTC' }); document.getElementById('historicalPeriod').textContent = `${formatDate(summary.data_period.start)} - ${formatDate(summary.data_period.end)}`; document.getElementById('forecastPeriod').textContent = `${formatDate(summary.forecast_period.start)} - ${formatDate(summary.forecast_period.end)}`; } /** * Creates or updates the main forecast chart. */ function createOrUpdateChart() { const ctx = document.getElementById('forecastChart').getContext('2d'); const chartData = prepareChartData(); const chartOptions = getChartOptions(); if (chartInstance) { chartInstance.data = chartData; chartInstance.options = chartOptions; chartInstance.update('none'); // 'none' = no animation on update } else { chartInstance = new Chart(ctx, { type: 'line', data: chartData, options: chartOptions, }); } } /** * Prepares the datasets for the chart, including actuals and all forecasts. * Filters data based on the selected year. */ function prepareChartData() { const { actuals_cumulative, cumulative_timelines } = forecastData; const datasets = []; // Filter actuals data for selected year (Jan 1 through current date or Dec 31) // Use UTC parsing to avoid timezone conversion issues const actualsData = actuals_cumulative .filter(d => { const dateStr = d.date.substring(0, 4); // Extract year as string "2025" return parseInt(dateStr) === selectedYear; }) .map(d => ({ x: new Date(d.date), y: d.cumulative_total })); // Determine the last fully completed month (first-of-month entry) for the selected year const lastActualMonthEntry = actuals_cumulative .filter(d => d.date.startsWith(`${selectedYear}-`) && d.date.endsWith('-01T00:00:00Z')) .reduce((latest, current) => { if (!latest) return current; return current.date > latest.date ? current : latest; }, null); const lastActualMonthDateStr = lastActualMonthEntry ? lastActualMonthEntry.date : null; console.log(`📊 Actuals data for ${selectedYear}:`, actualsData.length, 'points'); if (actualsData.length > 0) { console.log(' First:', actualsData[0]); console.log(' Last:', actualsData[actualsData.length - 1]); } datasets.push({ label: 'Actual CVEs', data: actualsData, borderColor: 'rgb(59, 130, 246)', borderWidth: 3, pointBackgroundColor: 'rgb(59, 130, 246)', tension: 0.1, fill: false, }); const { model_rankings } = forecastData; const bestColor = [22, 163, 74]; const worstColor = [200, 200, 200]; const interpolateColor = (color1, color2, factor) => { const result = color1.slice(); for (let i = 0; i < 3; i++) { result[i] = Math.round(result[i] + factor * (color2[i] - color1[i])); } return `rgb(${result.join(', ')})`; }; const topFiveModels = model_rankings.filter(m => cumulative_timelines[m.model_name + '_cumulative']).slice(0, TOP_MODELS_COUNT); topFiveModels.forEach((model, index) => { const modelKey = `${model.model_name}_cumulative`; // Filter forecast data by selected year // Use UTC parsing to avoid timezone conversion issues const modelData = cumulative_timelines[modelKey] .filter(d => { const dateStr = d.date.substring(0, 4); // Extract year as string "2025" const isSelectedYear = parseInt(dateStr) === selectedYear; // Exclude year boundary reset markers (Dec 31 with value 0, which belong to next year's view) const isResetMarker = d.date.includes('-12-31T23:59:59Z') && d.cumulative_total === 0; const entryDateStr = d.date; const isAfterActuals = !lastActualMonthDateStr || entryDateStr > lastActualMonthDateStr; return isSelectedYear && !isResetMarker && isAfterActuals; }) .map(d => ({ x: new Date(d.date), y: d.cumulative_total })); if (index === 0) { console.log(`📈 Forecast data for ${model.model_name} (${selectedYear}):`, modelData.length, 'points'); if (modelData.length > 0) { console.log(' First:', modelData[0]); console.log(' Last:', modelData[modelData.length - 1]); } } const factor = topFiveModels.length > 1 ? index / (topFiveModels.length - 1) : 0; const color = interpolateColor(bestColor, worstColor, factor); datasets.push({ label: `${model.model_name} (Forecast)`, data: modelData, borderColor: color, borderWidth: 2, pointBackgroundColor: color, borderDash: [5, 5], tension: 0.1, fill: false, hidden: index !== 0, }); }); if (cumulative_timelines.all_models_cumulative) { // Filter average forecast data by selected year // Use UTC parsing to avoid timezone conversion issues const avgData = cumulative_timelines.all_models_cumulative .filter(d => { const dateStr = d.date.substring(0, 4); // Extract year as string "2025" const isSelectedYear = parseInt(dateStr) === selectedYear; // Exclude year boundary reset markers (Dec 31 with value 0, which belong to next year's view) const isResetMarker = d.date.includes('-12-31T23:59:59Z') && d.cumulative_total === 0; const entryDateStr = d.date; const isAfterActuals = !lastActualMonthDateStr || entryDateStr > lastActualMonthDateStr; return isSelectedYear && !isResetMarker && isAfterActuals; }) .map(d => ({ x: new Date(d.date), y: d.cumulative_total })); datasets.push({ label: 'Model Average (Forecast)', data: avgData, borderColor: 'rgb(239, 68, 68)', borderWidth: 2, pointBackgroundColor: 'rgb(239, 68, 68)', borderDash: [5, 5], tension: 0.1, fill: false, hidden: false, }); } console.log(`Chart prepared with ${datasets.length} datasets.`); return { datasets }; } /** * Returns the configuration options for the chart. */ function getChartOptions() { return { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', labels: { usePointStyle: true, padding: 20 } }, tooltip: { mode: 'nearest', intersect: true, callbacks: { title: (tooltipItems) => { if (!tooltipItems.length) return ''; const pointDate = new Date(tooltipItems[0].parsed.x); // Check if this is a Dec 31 23:59:59 timestamp (End of Year marker) const month = pointDate.getUTCMonth(); const day = pointDate.getUTCDate(); const hour = pointDate.getUTCHours(); const minute = pointDate.getUTCMinutes(); const second = pointDate.getUTCSeconds(); if (month === 11 && day === 31 && hour === 23 && minute === 59 && second === 59) { return `End of Year ${pointDate.getUTCFullYear()}`; } // Check if it's the last point of the 'Actual CVEs' dataset const isLastActualPoint = tooltipItems[0].datasetIndex === 0 && tooltipItems[0].dataIndex === forecastData.actuals_cumulative.length - 1; if (isLastActualPoint) { return pointDate.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric', timeZone: 'UTC' }); } else { return pointDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric', timeZone: 'UTC' }); } }, label: (context) => { const label = context.dataset.label || ''; const cumulativeTotal = context.parsed.y; return `${label}: ${cumulativeTotal.toLocaleString()}`; }, }, }, }, scales: { x: { type: 'time', time: { unit: 'month', tooltipFormat: 'MMM yyyy' }, title: { display: true, text: 'Month' }, min: new Date(selectedYear - 1, 11, 25), // Dec 25 of previous year for padding max: new Date(selectedYear, 11, 31, 23, 59, 59), // Dec 31 at end of day }, y: { beginAtZero: true, title: { display: true, text: 'Number of CVEs' }, ticks: { callback: (value) => value.toLocaleString() }, }, }, }; } /** * Toggles the visibility of a model configuration row. * @param {number} index - The index of the model in the rankings array */ function toggleConfig(index) { const configRow = document.getElementById(`configRow${index}`); const expandBtn = document.getElementById(`expandBtn${index}`); if (configRow.classList.contains('hidden-row')) { configRow.classList.remove('hidden-row'); expandBtn.classList.add('expanded'); } else { configRow.classList.add('hidden-row'); expandBtn.classList.remove('expanded'); } } /** * Copies the configuration JSON to clipboard. * @param {number} index - The index of the model in the rankings array */ async function copyConfig(index) { const model = forecastData.model_rankings[index]; const config = formatModelConfigForDarts(model); const copyBtn = document.getElementById(`copyBtn${index}`); try { await navigator.clipboard.writeText(config); const originalText = copyBtn.textContent; copyBtn.textContent = 'Copied!'; copyBtn.classList.add('copied'); setTimeout(() => { copyBtn.textContent = originalText; copyBtn.classList.remove('copied'); }, 2000); } catch (err) { console.error('Failed to copy config:', err); // Fallback for browsers that don't support clipboard API const textArea = document.createElement('textarea'); textArea.value = config; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); copyBtn.textContent = 'Copied!'; copyBtn.classList.add('copied'); setTimeout(() => { copyBtn.textContent = 'Copy JSON'; copyBtn.classList.remove('copied'); }, 2000); } } /** * Formats model configuration for display. * @param {Object} model - The model object from rankings * @returns {string} Formatted configuration string */ function formatModelConfig(model) { // Priority: Show hyperparameters first if available, then performance metrics if (model.hyperparameters && Object.keys(model.hyperparameters).length > 0) { // Clean up hyperparameters by removing null values and formatting const cleanedHyperparameters = {}; for (const [key, value] of Object.entries(model.hyperparameters)) { if (value !== null && value !== undefined) { cleanedHyperparameters[key] = value; } } const config = { model_name: model.model_name, hyperparameters: cleanedHyperparameters, split_ratio: parseFloat((model.split_ratio || 0.8).toFixed(3)) }; // Add tuning metadata if available if (model.tuned_at) { config.tuned_at = model.tuned_at; } // Add performance metrics as secondary info config.performance_metrics = { mape: parseFloat((model.mape || 0).toFixed(4)), mae: parseFloat((model.mae || 0).toFixed(4)) }; // Add training_time if available if (model.training_time !== undefined && model.training_time !== null) { config.performance_metrics.training_time = parseFloat(model.training_time.toFixed(6)); } return syntaxHighlightJSON(JSON.stringify(config, null, 2).trim()); } else { // Fallback for legacy data without hyperparameters const config = { model_name: model.model_name, split_ratio: parseFloat((model.split_ratio || 0.8).toFixed(3)), performance_metrics: { mape: parseFloat((model.mape || 0).toFixed(4)), mae: parseFloat((model.mae || 0).toFixed(4)) }, note: "Run comprehensive tuner to generate optimal hyperparameters" }; // Add training_time if available if (model.training_time !== undefined && model.training_time !== null) { config.performance_metrics.training_time = parseFloat(model.training_time.toFixed(6)); } return syntaxHighlightJSON(JSON.stringify(config, null, 2).trim()); } } /** * Adds syntax highlighting to JSON string. * @param {string} json - The JSON string to highlight * @returns {string} HTML with syntax highlighting */ function syntaxHighlightJSON(json) { // Escape HTML first json = json.replace(/&/g, '&').replace(//g, '>'); return json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) { let cls = 'json-number'; if (/^"/.test(match)) { if (/:$/.test(match)) { cls = 'json-key'; } else { cls = 'json-string'; } } else if (/true|false/.test(match)) { cls = 'json-boolean'; } else if (/null/.test(match)) { cls = 'json-null'; } return '' + match + ''; }) .replace(/([{}[\],])/g, '$1'); } /** * Formats model configuration for Darts usage. * @param {Object} model - The model object from rankings * @returns {string} Darts-compatible configuration string */ function formatModelConfigForDarts(model) { if (model.hyperparameters && Object.keys(model.hyperparameters).length > 0) { // Clean up hyperparameters by removing null values const cleanedHyperparameters = {}; for (const [key, value] of Object.entries(model.hyperparameters)) { if (value !== null && value !== undefined) { cleanedHyperparameters[key] = value; } } // Full configuration with hyperparameters const dartsConfig = { model: model.model_name, hyperparameters: cleanedHyperparameters, split_ratio: parseFloat((model.split_ratio || 0.8).toFixed(3)), expected_performance: { mape: parseFloat((model.mape || 0).toFixed(4)), mae: parseFloat((model.mae || 0).toFixed(4)) } }; // Add training_time if available if (model.training_time !== undefined && model.training_time !== null) { dartsConfig.expected_performance.training_time = parseFloat(model.training_time.toFixed(6)); } return JSON.stringify(dartsConfig, null, 2); } else { // Fallback for legacy data without hyperparameters const dartsConfig = { model: model.model_name, split_ratio: parseFloat((model.split_ratio || 0.8).toFixed(3)), expected_performance: { mape: parseFloat((model.mape || 0).toFixed(4)), mae: parseFloat((model.mae || 0).toFixed(4)) }, note: "No hyperparameters available - run comprehensive tuner for optimal parameters" }; // Add training_time if available if (model.training_time !== undefined && model.training_time !== null) { dartsConfig.expected_performance.training_time = parseFloat(model.training_time.toFixed(6)); } return JSON.stringify(dartsConfig, null, 2); } }