/** * Chronos-2 Zero-Shot Demo - Frontend * Real-time input -> debounce -> fetch forecast -> chart update * Tab: Forecast | Compare */ (function () { const API_BASE = ''; const tsInput = document.getElementById('ts-input'); const sampleSelect = document.getElementById('sample-select'); const predStepsInput = document.getElementById('pred-steps'); const forecastValue = document.getElementById('forecast-value'); const forecastRange = document.getElementById('forecast-range'); const loadingEl = document.getElementById('loading'); const errorEl = document.getElementById('error'); const downloadBtn = document.getElementById('download-csv'); let chart = null; let lastResult = null; let debounceTimer = null; // --------------------------------------------------------------------------- // Tabs // --------------------------------------------------------------------------- function initTabs() { const tabBtns = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); tabBtns.forEach((btn) => { btn.addEventListener('click', () => { const tabId = btn.getAttribute('data-tab'); tabBtns.forEach((b) => b.classList.remove('active')); tabContents.forEach((c) => { c.classList.toggle('active', c.id === 'tab-' + tabId); }); btn.classList.add('active'); }); }); } initTabs(); // --------------------------------------------------------------------------- // Parse // --------------------------------------------------------------------------- function parseValues(text) { if (!text || !text.trim()) return []; const parts = text.trim().split(/[\s,;\n]+/); const values = []; for (const p of parts) { const n = parseFloat(p); if (!isNaN(n)) values.push(n); } return values; } function debounce(fn, ms) { return function () { clearTimeout(debounceTimer); debounceTimer = setTimeout(fn, ms); }; } async function fetchForecast(values, predictionLength) { const res = await fetch(`${API_BASE}/api/forecast`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ values, prediction_length: predictionLength }), }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })); const d = err.detail; const msg = Array.isArray(d) ? d.map((x) => x.msg || JSON.stringify(x)).join('; ') : typeof d === 'string' ? d : JSON.stringify(d || err); throw new Error(msg || `HTTP ${res.status}`); } return res.json(); } function setLoading(loading) { loadingEl.style.display = loading ? 'block' : 'none'; errorEl.style.display = 'none'; } function setError(msg) { errorEl.textContent = msg; errorEl.style.display = msg ? 'block' : 'none'; loadingEl.style.display = 'none'; } function updateForecastDisplay(result) { downloadBtn.disabled = !result || !result.forecast.length; if (!result || !result.forecast.length) { forecastValue.textContent = '—'; forecastRange.textContent = ''; return; } const f = result.forecast[0]; forecastValue.textContent = f.median.toFixed(2); forecastRange.textContent = ` (${f.low.toFixed(1)} ~ ${f.high.toFixed(1)})`; if (result.forecast.length > 1) { forecastRange.textContent += ` · ${result.forecast.length} steps`; } } function updateChart(historical, forecast) { const histLabels = (historical || []).map((_, i) => String(i)); const histData = (historical || []).map((h) => h.value); const forecastLabels = (forecast || []).map((f) => String(f.index)); const forecastMedians = (forecast || []).map((f) => f.median); const allLabels = histLabels.concat(forecastLabels); const histFull = histData.concat(forecastMedians.map(() => null)); const fcastFull = histLabels.map(() => null).concat(forecastMedians); if (!chart) { chart = new Chart(document.getElementById('chart'), { type: 'line', data: { labels: allLabels, datasets: [ { label: 'Historical', data: histFull, borderColor: 'rgb(59, 130, 246)', fill: false, tension: 0.2, }, { label: 'Forecast (median)', data: fcastFull, borderColor: 'rgb(239, 68, 68)', borderDash: [5, 5], fill: false, tension: 0.2, }, ], }, options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'top' } }, scales: { x: { title: { display: true, text: 'Index' } }, y: { title: { display: true, text: 'Value' } }, }, }, }); } else { chart.data.labels = allLabels; chart.data.datasets[0].data = histFull; chart.data.datasets[1].data = fcastFull; chart.update(); } } async function triggerForecast() { const values = parseValues(tsInput.value); const predictionLength = Math.max(1, parseInt(predStepsInput.value, 10) || 1); if (values.length === 0) { lastResult = null; updateForecastDisplay(null); updateChart([], []); setError(''); return; } setLoading(true); setError(''); try { const result = await fetchForecast(values, predictionLength); lastResult = result; updateForecastDisplay(result); updateChart(result.historical, result.forecast); } catch (err) { setError(err.message || '예측 중 오류가 발생했습니다.'); lastResult = null; updateForecastDisplay(null); updateChart( values.map((v, i) => ({ index: i, value: v })), [] ); } finally { setLoading(false); } } const debouncedForecast = debounce(triggerForecast, 500); async function loadSample(name) { try { const res = await fetch(`${API_BASE}/static/samples/${name}.json`); if (!res.ok) throw new Error('Sample not found'); const data = await res.json(); const values = data.values || data; tsInput.value = Array.isArray(values) ? values.join(', ') : String(values); debouncedForecast(); } catch (err) { setError('샘플을 불러올 수 없습니다.'); } } function downloadCsv() { if (!lastResult || !lastResult.forecast.length) return; const headers = ['index', 'median', 'low', 'high']; const rows = lastResult.forecast.map((f) => [f.index, f.median, f.low, f.high].join(',')); const csv = [headers.join(','), ...rows].join('\n'); const blob = new Blob([csv], { type: 'text/csv' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'chronos2_forecast.csv'; a.click(); URL.revokeObjectURL(a.href); } tsInput.addEventListener('input', debouncedForecast); predStepsInput.addEventListener('change', debouncedForecast); sampleSelect.addEventListener('change', function () { const v = this.value; if (v) { loadSample(v); this.value = ''; } }); downloadBtn.addEventListener('click', downloadCsv); updateForecastDisplay(null); updateChart([], []); // 페이지 로드 시 GFK 샘플 데이터를 기본값으로 로드 loadSample('gfk_sample'); })();