DeryFerd commited on
Commit
ab2ded9
·
verified ·
1 Parent(s): 95f07d9

Upload index.html

Browse files
Files changed (1) hide show
  1. frontend /index.html +491 -0
frontend /index.html ADDED
@@ -0,0 +1,491 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>Financial Risk & Forecast</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
8
+ <style>
9
+ :root {
10
+ --bg-primary: #f9fafb;
11
+ --bg-secondary: #ffffff;
12
+ --text-primary: #1f2937;
13
+ --text-secondary: #6b7280;
14
+ --accent-blue: #3b82f6;
15
+ --accent-blue-dark: #1d4ed8;
16
+ --accent-purple: #8b5cf6;
17
+ --accent-red: #ef4444;
18
+ --accent-green: #10b981;
19
+ --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
20
+ --border-color: #e5e7eb;
21
+ --card-bg: #ffffff;
22
+ --navbar-bg: #ffffff;
23
+ }
24
+
25
+ .dark-mode {
26
+ --bg-primary: #111827;
27
+ --bg-secondary: #1f2937;
28
+ --text-primary: #f9fafb;
29
+ --text-secondary: #d1d5db;
30
+ --border-color: #374151;
31
+ --card-bg: #1f2937;
32
+ --navbar-bg: #1f2937;
33
+ }
34
+
35
+ * { margin: 0; padding: 0; box-sizing: border-box; }
36
+ body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: var(--bg-primary); color: var(--text-primary); line-height: 1.6; transition: background-color 0.3s, color 0.3s; }
37
+ header { background-color: var(--navbar-bg); box-shadow: var(--shadow); position: sticky; top: 0; z-index: 100; }
38
+ .container { width: 100%; max-width: 1200px; margin: 0 auto; padding: 0 20px; }
39
+ .navbar { display: flex; justify-content: space-between; align-items: center; padding: 1.5rem 0; }
40
+ .logo { display: flex; align-items: center; gap: 10px; font-size: 1.5rem; font-weight: bold; color: var(--accent-blue); }
41
+ .nav-links { display: flex; gap: 2rem; }
42
+ .nav-links a { color: var(--text-secondary); text-decoration: none; font-weight: 500; transition: color 0.2s; cursor: pointer; }
43
+ .nav-links a.active, .nav-links a:hover { color: var(--accent-blue); }
44
+ .dark-mode-toggle { background: none; border: none; cursor: pointer; padding: 8px; border-radius: 50%; display: flex; align-items: center; justify-content: center; width: 40px; height: 40px; background-color: var(--bg-secondary); box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
45
+ .page { min-height: calc(100vh - 180px); padding: 2rem 0; display: none; }
46
+ .page.active { display: block; }
47
+ .hero { text-align: center; padding: 4rem 0; }
48
+ .hero h1 { font-size: 2.5rem; margin-bottom: 1rem; background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }
49
+ .hero p { font-size: 1.2rem; color: var(--text-secondary); max-width: 700px; margin: 0 auto 2rem; }
50
+ .btn { display: inline-block; background: linear-gradient(90deg, var(--accent-blue), var(--accent-blue-dark)); color: white; padding: 12px 32px; border-radius: 9999px; font-weight: 600; text-decoration: none; box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3); transition: all 0.3s ease; border: none; cursor: pointer; }
51
+ .btn:hover { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(59, 130, 246, 0.4); }
52
+ .card { background-color: var(--card-bg); border-radius: 12px; box-shadow: var(--shadow); padding: 1.5rem; margin-bottom: 1.5rem; border: 1px solid var(--border-color); }
53
+ .form-group { margin-bottom: 1.5rem; }
54
+ .form-group label { display: block; margin-bottom: 0.5rem; font-weight: 500; color: var(--text-primary); }
55
+ .form-control { width: 100%; padding: 12px; border: 1px solid var(--border-color); border-radius: 8px; font-size: 1rem; background-color: var(--bg-secondary); color: var(--text-primary); transition: border-color 0.3s; }
56
+ .form-control:focus { outline: none; border-color: var(--accent-blue); box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2); }
57
+ .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
58
+ .results-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
59
+ .result-card { text-align: center; padding: 1.5rem; border-radius: 12px; background: var(--card-bg); border: 1px solid var(--border-color); }
60
+ .result-value { font-size: 2rem; font-weight: bold; margin: 0.5rem 0; }
61
+ .result-label { color: var(--text-secondary); font-size: 1.1rem; }
62
+ .return-value { color: var(--accent-green); }
63
+ .volatility-value { color: var(--accent-red); }
64
+ .sharpe-value { color: var(--accent-blue); }
65
+ .chart-container { height: 350px; position: relative; }
66
+ .footer { background-color: var(--navbar-bg); padding: 2rem 0; margin-top: 4rem; }
67
+ .footer p { text-align: center; color: var(--text-secondary); font-size: 0.9rem; }
68
+ .loading { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 50vh; gap: 1rem; }
69
+ .spinner { width: 50px; height: 50px; border: 5px solid #f3f3f3; border-top: 5px solid var(--accent-blue); border-radius: 50%; animation: spin 1s linear infinite; }
70
+ @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
71
+ .disclaimer { background-color: #fef3c7; border-left: 4px solid #f59e0b; padding: 1rem; border-radius: 0 8px 8px 0; margin: 1.5rem 0; }
72
+ .disclaimer p { color: #92400e; font-size: 0.95rem; font-weight: 500; }
73
+ .dark-mode .disclaimer { background-color: #373022; border-color: #f59e0b; }
74
+ .dark-mode .disclaimer p { color: #fef3c7; }
75
+ .back-link { display: inline-block; margin-top: 1rem; color: var(--accent-blue); text-decoration: none; font-weight: 500; cursor: pointer; }
76
+ .back-link:hover { text-decoration: underline; }
77
+ .hidden { display: none !important; }
78
+ .btn-secondary { background-color: #e5e7eb; color: #4b5563; padding: 8px 16px; border: none; border-radius: 8px; font-weight: 500; cursor: pointer; }
79
+ .btn-secondary:hover { background-color: #d1d5db; }
80
+ </style>
81
+ </head>
82
+ <body>
83
+ <header>
84
+ <div class="container">
85
+ <nav class="navbar">
86
+ <div class="logo">
87
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
88
+ <line x1="18" y1="20" x2="18" y2="10"></line><line x1="12" y1="20" x2="12" y2="4"></line><line x1="6" y1="20" x2="6" y2="14"></line>
89
+ </svg>
90
+ RiskForecaster
91
+ </div>
92
+ <div class="nav-links">
93
+ <a class="nav-link active" data-page="home">Beranda</a>
94
+ <a class="nav-link" data-page="input">Input Data</a>
95
+ <a class="nav-link" data-page="about">Tentang</a>
96
+ </div>
97
+ <button class="dark-mode-toggle" id="darkModeToggle">
98
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="sun-icon">
99
+ <circle cx="12" cy="12" r="5"></circle><line x1="12" y1="1" x2="12" y2="3"></line><line x1="12" y1="21" x2="12" y2="23"></line><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line><line x1="1" y1="12" x2="3" y2="12"></line><line x1="21" y1="12" x2="23" y2="12"></line><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
100
+ </svg>
101
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="moon-icon hidden">
102
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
103
+ </svg>
104
+ </button>
105
+ </nav>
106
+ </div>
107
+ </header>
108
+
109
+ <main class="container">
110
+ <section id="home" class="page active">
111
+ <div class="hero">
112
+ <h1>Portfolio Risk & Forecast</h1>
113
+ <p>Analisis risiko portofolio dan dapatkan prediksi harga menggunakan Machine Learning.</p>
114
+ <button class="btn" id="startBtn">Mulai Analisis</button>
115
+ </div>
116
+ </section>
117
+
118
+ <section id="input" class="page">
119
+ <div class="card">
120
+ <h2 style="font-size: 1.8rem; margin-bottom: 1.5rem; color: var(--text-primary);">Input Data Portofolio</h2>
121
+
122
+ <div class="form-group">
123
+ <label for="tickers">Ticker Saham (pisahkan dengan koma)</label>
124
+ <input type="text" id="tickers" class="form-control" placeholder="Contoh: BBCA.JK,AAPL,TSLA" value="BBCA.JK">
125
+ </div>
126
+
127
+ <div class="grid-2">
128
+ <div class="form-group">
129
+ <label for="start_date">Tanggal Mulai</label>
130
+ <input type="date" id="start_date" class="form-control" value="2022-01-01">
131
+ </div>
132
+ <div class="form-group">
133
+ <label for="end_date">Tanggal Selesai</label>
134
+ <input type="date" id="end_date" class="form-control">
135
+ </div>
136
+ </div>
137
+ <div id="error-message" class="hidden" style="color: var(--accent-red); text-align: center; margin-bottom: 1rem;"></div>
138
+ <button class="btn" id="calculateBtn">Hitung Risiko & Prediksi</button>
139
+ </div>
140
+ <a class="back-link" id="backHome">← Kembali ke Beranda</a>
141
+ </section>
142
+
143
+ <section id="results" class="page">
144
+ <div class="card" id="resultsContent">
145
+ <h2 style="font-size: 1.8rem; margin-bottom: 1.5rem; color: var(--text-primary); text-align: center;">Hasil Analisis Portofolio</h2>
146
+ <p id="portfolioInfo" style="text-align: center; color: var(--text-secondary); margin-bottom: 2rem;"></p>
147
+
148
+ <div class="results-grid">
149
+ <div class="result-card">
150
+ <div class="result-label">Expected Annual Return</div>
151
+ <div class="result-value return-value" id="returnValue">0%</div>
152
+ <div style="color: var(--text-secondary); font-size: 0.9rem;">Prediksi imbal hasil tahunan</div>
153
+ </div>
154
+
155
+ <div class="result-card">
156
+ <div class="result-label">Annual Volatility (Risk)</div>
157
+ <div class="result-value volatility-value" id="volatilityValue">0%</div>
158
+ <div style="color: var(--text-secondary); font-size: 0.9rem;">Tingkat risiko fluktuasi harga</div>
159
+ </div>
160
+
161
+ <div class="result-card">
162
+ <div class="result-label">Sharpe Ratio</div>
163
+ <div class="result-value sharpe-value" id="sharpeValue">0.00</div>
164
+ <div style="color: var(--text-secondary); font-size: 0.9rem;">Imbal hasil per unit risiko</div>
165
+ </div>
166
+ </div>
167
+
168
+ <div class="card">
169
+ <h3 style="margin-bottom: 1rem; color: var(--text-primary);" id="forecastTitle">Prediksi Harga Saham</h3>
170
+ <div class="chart-container">
171
+ <canvas id="forecastChart"></canvas>
172
+ </div>
173
+ </div>
174
+
175
+ <div style="text-align: center; margin-top: 2rem;">
176
+ <button class="btn btn-secondary" id="analyzeAgain">Analisis Portofolio Lain</button>
177
+ </div>
178
+ </div>
179
+
180
+ <div id="loading" class="loading hidden">
181
+ <div class="spinner"></div>
182
+ <p style="color: var(--text-secondary); font-size: 1.1rem;">Menghitung risiko & prediksi...</p>
183
+ <p style="color: var(--text-secondary);">Ini bisa memakan beberapa saat, harap tunggu.</p>
184
+ </div>
185
+ </section>
186
+
187
+ <section id="about" class="page">
188
+ </section>
189
+ </main>
190
+
191
+ <footer class="footer">
192
+ </footer>
193
+
194
+ <script>
195
+ document.addEventListener('DOMContentLoaded', () => {
196
+ // --- REVISI 3: Deklarasi elemen & logika dipindahkan ke dalam 'DOMContentLoaded' ---
197
+ const API_BASE_URL = 'https://psychic-goldfish-jvvvxjrwgp9hg56-8000.app.github.dev';
198
+ // DOM Elements
199
+ const pages = document.querySelectorAll('.page');
200
+ const navLinks = document.querySelectorAll('.nav-link');
201
+ const startBtn = document.getElementById('startBtn');
202
+ const backHome = document.getElementById('backHome');
203
+ const calculateBtn = document.getElementById('calculateBtn');
204
+ const analyzeAgain = document.getElementById('analyzeAgain');
205
+ const darkModeToggle = document.getElementById('darkModeToggle');
206
+ const sunIcon = document.querySelector('.sun-icon');
207
+ const moonIcon = document.querySelector('.moon-icon');
208
+ const resultsContent = document.getElementById('resultsContent');
209
+ const loading = document.getElementById('loading');
210
+ const errorMessage = document.getElementById('error-message');
211
+
212
+ // Form Input Elements
213
+ const tickersInput = document.getElementById('tickers');
214
+ const startDateInput = document.getElementById('start_date');
215
+ const endDateInput = document.getElementById('end_date');
216
+
217
+ // Result elements
218
+ const portfolioInfo = document.getElementById('portfolioInfo');
219
+ const returnValueEl = document.getElementById('returnValue');
220
+ const volatilityValueEl = document.getElementById('volatilityValue');
221
+ const sharpeValueEl = document.getElementById('sharpeValue');
222
+ const forecastTitleEl = document.getElementById('forecastTitle');
223
+
224
+ // Charts
225
+ let forecastChartInstance = null;
226
+
227
+ // GANTI DENGAN KODE INI
228
+ // Atur tanggal selesai secara manual ke tanggal valid di masa lalu untuk development
229
+ endDateInput.value = '2025-08-29';
230
+ console.log("Tanggal selesai diatur secara manual ke 2024-08-30 untuk menghindari data masa depan.");
231
+
232
+ // Dark Mode Toggle
233
+ darkModeToggle.addEventListener('click', () => {
234
+ document.body.classList.toggle('dark-mode');
235
+ sunIcon.classList.toggle('hidden');
236
+ moonIcon.classList.toggle('hidden');
237
+ localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
238
+ // Re-render chart with correct colors if it exists
239
+ if (forecastChartInstance) {
240
+ forecastChartInstance.destroy();
241
+ // NOTE: Ideally, re-render chart with new colors. For simplicity, we just destroy it.
242
+ // A better implementation would be to store chart data and call a render function.
243
+ }
244
+ });
245
+
246
+ if (localStorage.getItem('darkMode') === 'true') {
247
+ document.body.classList.add('dark-mode');
248
+ sunIcon.classList.add('hidden');
249
+ moonIcon.classList.remove('hidden');
250
+ }
251
+
252
+ // Navigation
253
+ function navigateTo(pageId) {
254
+ pages.forEach(page => page.classList.remove('active'));
255
+ navLinks.forEach(link => link.classList.remove('active'));
256
+ document.getElementById(pageId).classList.add('active');
257
+ const activeLink = document.querySelector(`.nav-link[data-page="${pageId}"]`);
258
+ if (activeLink) activeLink.classList.add('active');
259
+ }
260
+
261
+ navLinks.forEach(link => {
262
+ link.addEventListener('click', (e) => {
263
+ e.preventDefault();
264
+ const pageId = link.getAttribute('data-page');
265
+ navigateTo(pageId);
266
+ });
267
+ });
268
+
269
+ startBtn.addEventListener('click', () => navigateTo('input'));
270
+ backHome.addEventListener('click', (e) => { e.preventDefault(); navigateTo('home'); });
271
+ analyzeAgain.addEventListener('click', () => navigateTo('input'));
272
+
273
+ // --- REVISI 4: Ganti logika kalkulasi dengan panggilan API ---
274
+ // GANTI SELURUH FUNGSI calculateBtn.addEventListener DENGAN VERSI FINAL INI
275
+ calculateBtn.addEventListener('click', async () => {
276
+ const tickers = tickersInput.value.trim();
277
+ if (!tickers) {
278
+ alert('Silakan masukkan minimal satu ticker saham.');
279
+ return;
280
+ }
281
+
282
+ const tickersArray = tickers.split(',').map(t => t.trim());
283
+ const requestBody = {
284
+ tickers: tickersArray,
285
+ start_date: startDateInput.value,
286
+ end_date: endDateInput.value
287
+ };
288
+
289
+ resultsContent.classList.add('hidden');
290
+ loading.classList.remove('hidden');
291
+ errorMessage.classList.add('hidden');
292
+ navigateTo('results');
293
+
294
+ console.log("Memulai proses... Menghitung Risiko...");
295
+
296
+ try {
297
+ // Langkah 1: Ambil data risiko terlebih dahulu
298
+ const riskData = await fetchRiskData(requestBody);
299
+ console.log("✅ Selesai menghitung risiko.");
300
+
301
+ // Jika riskData berisi error, hentikan proses
302
+ if (riskData.error) {
303
+ throw new Error(riskData.error);
304
+ }
305
+
306
+ // Langkah 2: Baru setelah risiko berhasil, ambil data prediksi
307
+ console.log("Sekarang mengambil data prediksi...");
308
+ const forecastData = await fetchForecastData({ ...requestBody, ticker: tickersArray[0] });
309
+ console.log("✅ Selesai mengambil data prediksi.");
310
+
311
+ // Jika forecastData berisi error, kita tetap bisa menampilkan risk, tapi beri peringatan
312
+ if (forecastData.error) {
313
+ // Tampilkan metrik risiko yang berhasil
314
+ updateRiskMetrics(riskData, tickers);
315
+ // Tampilkan pesan error di area chart
316
+ updateForecastChart({ error: forecastData.error }, tickersArray[0]);
317
+ } else {
318
+ // Jika semua berhasil, tampilkan semuanya
319
+ updateRiskMetrics(riskData, tickers);
320
+ updateForecastChart(forecastData, tickersArray[0]);
321
+ }
322
+
323
+ resultsContent.classList.remove('hidden');
324
+
325
+ } catch (error) {
326
+ console.error('API Error:', error);
327
+ errorMessage.textContent = `Error: ${error.message}`;
328
+ errorMessage.classList.remove('hidden');
329
+ navigateTo('input');
330
+ } finally {
331
+ console.log("Proses selesai, menyembunyikan loading spinner.");
332
+ loading.classList.add('hidden');
333
+ }
334
+ });
335
+
336
+ // --- REVISI 5: Buat fungsi-fungsi baru untuk fetch API dan update UI ---
337
+
338
+ async function fetchRiskData(body) {
339
+ const response = await fetch(`${API_BASE_URL}/calculate-risk`, {
340
+ method: 'POST',
341
+ headers: { 'Content-Type': 'application/json' },
342
+ body: JSON.stringify(body)
343
+ });
344
+ if (!response.ok) {
345
+ const errorData = await response.json();
346
+ throw new Error(errorData.detail || `Gagal mengambil data risiko.`);
347
+ }
348
+ return response.json();
349
+ }
350
+
351
+ async function fetchForecastData(body) {
352
+ const forecastBody = {
353
+ ticker: body.ticker,
354
+ start_date: body.start_date,
355
+ end_date: body.end_date,
356
+ forecast_horizon: 30
357
+ };
358
+ const response = await fetch(`${API_BASE_URL}/get-forecast`, {
359
+ method: 'POST',
360
+ headers: { 'Content-Type': 'application/json' },
361
+ body: JSON.stringify(forecastBody)
362
+ });
363
+ if (!response.ok) {
364
+ const errorData = await response.json();
365
+ throw new Error(errorData.detail || `Gagal mengambil prediksi untuk ${body.ticker}.`);
366
+ }
367
+ return response.json();
368
+ }
369
+
370
+ function formatPercent(value) {
371
+ return `${(value * 100).toFixed(2)}%`;
372
+ }
373
+
374
+ function updateRiskMetrics(data, tickers) {
375
+ portfolioInfo.textContent = `Portofolio: ${tickers.toUpperCase()}`;
376
+ returnValueEl.textContent = formatPercent(data.expected_annual_return);
377
+ volatilityValueEl.textContent = formatPercent(data.annual_volatility);
378
+ sharpeValueEl.textContent = data.sharpe_ratio.toFixed(2);
379
+ }
380
+
381
+ function updateForecastChart(data, ticker) {
382
+ if (forecastChartInstance) {
383
+ forecastChartInstance.destroy();
384
+ }
385
+
386
+ // Cek jika data yang diterima adalah objek error dari backend
387
+ if (data.error) {
388
+ console.error("Gagal mendapatkan data prediksi:", data.error);
389
+ forecastTitleEl.textContent = `Gagal memuat data prediksi untuk ${ticker.toUpperCase()}`;
390
+ // Anda bisa menambahkan pesan di dalam canvas di sini jika mau
391
+ return;
392
+ }
393
+
394
+
395
+ let chartData; // Variabel untuk menampung data yang formatnya sudah benar
396
+
397
+ // Cek #1: Apakah data sudah dalam format Array yang benar?
398
+ if (Array.isArray(data)) {
399
+ console.log("Format data sudah benar (Array).");
400
+ chartData = data;
401
+ }
402
+ // Cek #2: Jika bukan Array, apakah ini format Object yang kita duga?
403
+ else if (data && data.ds && data.yhat) {
404
+ console.warn("Data bukan Array, mencoba mengubah format dari Object...");
405
+ // Ubah format dari {ds: [...], yhat: [...]} menjadi [{ds: ..., yhat: ...}, ...]
406
+ chartData = data.ds.map((date, index) => ({
407
+ ds: date,
408
+ yhat: data.yhat[index],
409
+ yhat_lower: data.yhat_lower[index],
410
+ yhat_upper: data.yhat_upper[index],
411
+ }));
412
+ } else {
413
+ // Jika format tidak dikenali, tampilkan error dan hentikan fungsi
414
+ console.error("Format data prediksi tidak dikenali atau data kosong.");
415
+ forecastTitleEl.textContent = `Gagal memuat data prediksi untuk ${ticker.toUpperCase()}`;
416
+ if (forecastChartInstance) forecastChartInstance.destroy(); // Hapus chart lama jika ada
417
+ return;
418
+ }
419
+
420
+ if (forecastChartInstance) {
421
+ forecastChartInstance.destroy();
422
+ }
423
+
424
+ // Sekarang, kita gunakan chartData yang formatnya sudah pasti benar (Array)
425
+ const labels = chartData.map(d => d.ds);
426
+ const predictions = chartData.map(d => d.yhat);
427
+ const lowerBounds = chartData.map(d => d.yhat_lower);
428
+ const upperBounds = chartData.map(d => d.yhat_upper);
429
+
430
+ forecastTitleEl.textContent = `Prediksi Harga 30 Hari ke Depan untuk ${ticker.toUpperCase()}`;
431
+
432
+
433
+ const ctx = document.getElementById('forecastChart').getContext('2d');
434
+ forecastChartInstance = new Chart(ctx, {
435
+ type: 'line',
436
+ data: {
437
+ labels: labels,
438
+ datasets: [
439
+ {
440
+ label: 'Prediksi Harga',
441
+ data: predictions,
442
+ borderColor: 'rgba(59, 130, 246, 1)',
443
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
444
+ borderWidth: 2,
445
+ tension: 0.1,
446
+ fill: false,
447
+ pointRadius: 0
448
+ },
449
+ {
450
+ label: 'Batas Atas',
451
+ data: upperBounds,
452
+ borderColor: 'rgba(156, 163, 175, 0.5)',
453
+ borderWidth: 1,
454
+ borderDash: [5, 5],
455
+ fill: false,
456
+ pointRadius: 0
457
+ },
458
+ {
459
+ label: 'Batas Bawah',
460
+ data: lowerBounds,
461
+ borderColor: 'rgba(156, 163, 175, 0.5)',
462
+ backgroundColor: 'rgba(156, 163, 175, 0.05)',
463
+ borderWidth: 1,
464
+ borderDash: [5, 5],
465
+ fill: '-1', // Fill to the dataset above (upperBounds)
466
+ pointRadius: 0
467
+ }
468
+ ]
469
+ },
470
+ options: {
471
+ responsive: true,
472
+ maintainAspectRatio: false,
473
+ plugins: { legend: { display: false } },
474
+ scales: {
475
+ y: { ticks: { color: document.body.classList.contains('dark-mode') ? '#d1d5db' : '#6b7280' } },
476
+ x: {
477
+ ticks: {
478
+ color: document.body.classList.contains('dark-mode') ? '#d1d5db' : '#6b7280',
479
+ maxRotation: 0,
480
+ autoSkip: true,
481
+ maxTicksLimit: 6
482
+ }
483
+ }
484
+ }
485
+ }
486
+ });
487
+ }
488
+ });
489
+ </script>
490
+ </body>
491
+ </html>