Spaces:
No application file
No application file
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Castor Price Forecast Dashboard</title> | |
| <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 15px; | |
| box-shadow: 0 10px 40px rgba(0,0,0,0.3); | |
| padding: 30px; | |
| } | |
| .header { | |
| text-align: center; | |
| margin-bottom: 30px; | |
| border-bottom: 3px solid #667eea; | |
| padding-bottom: 20px; | |
| } | |
| .header h1 { | |
| color: #333; | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| } | |
| .header p { | |
| color: #666; | |
| font-size: 1.1em; | |
| } | |
| .controls { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 15px; | |
| margin-bottom: 30px; | |
| padding: 20px; | |
| background: #f8f9fa; | |
| border-radius: 10px; | |
| } | |
| .control-group { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .control-group label { | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 5px; | |
| font-size: 0.9em; | |
| } | |
| .control-group input, | |
| .control-group select { | |
| padding: 10px; | |
| border: 2px solid #ddd; | |
| border-radius: 5px; | |
| font-size: 1em; | |
| transition: border-color 0.3s; | |
| } | |
| .control-group input:focus, | |
| .control-group select:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| box-shadow: 0 0 5px rgba(102, 126, 234, 0.3); | |
| } | |
| .button-group { | |
| display: flex; | |
| gap: 10px; | |
| align-items: flex-end; | |
| } | |
| button { | |
| padding: 10px 20px; | |
| background: #667eea; | |
| color: white; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-size: 1em; | |
| font-weight: 600; | |
| transition: background 0.3s, transform 0.2s; | |
| } | |
| button:hover { | |
| background: #764ba2; | |
| transform: translateY(-2px); | |
| } | |
| button:active { | |
| transform: translateY(0); | |
| } | |
| .stats { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 15px; | |
| margin-bottom: 30px; | |
| } | |
| .stat-card { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 20px; | |
| border-radius: 10px; | |
| text-align: center; | |
| } | |
| .stat-card h3 { | |
| font-size: 0.9em; | |
| opacity: 0.9; | |
| margin-bottom: 10px; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .stat-card .value { | |
| font-size: 2em; | |
| font-weight: bold; | |
| } | |
| .charts-grid { | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| gap: 30px; | |
| margin-bottom: 30px; | |
| } | |
| .chart-container { | |
| background: #f8f9fa; | |
| border-radius: 10px; | |
| padding: 20px; | |
| box-shadow: 0 3px 10px rgba(0,0,0,0.1); | |
| } | |
| .chart-title { | |
| font-size: 1.3em; | |
| font-weight: 600; | |
| color: #333; | |
| margin-bottom: 15px; | |
| border-left: 4px solid #667eea; | |
| padding-left: 10px; | |
| } | |
| #mainChart, #arimaChart, #lstmChart, #comparisonChart { | |
| width: 100% ; | |
| height: 500px; | |
| } | |
| .loading { | |
| text-align: center; | |
| padding: 40px; | |
| color: #667eea; | |
| font-size: 1.2em; | |
| } | |
| .error { | |
| background: #fee; | |
| color: #c33; | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin-bottom: 20px; | |
| border-left: 4px solid #c33; | |
| } | |
| .success { | |
| background: #efe; | |
| color: #3c3; | |
| padding: 20px; | |
| border-radius: 10px; | |
| margin-bottom: 20px; | |
| border-left: 4px solid #3c3; | |
| } | |
| @media (max-width: 768px) { | |
| .header h1 { | |
| font-size: 1.8em; | |
| } | |
| .controls { | |
| grid-template-columns: 1fr; | |
| } | |
| #mainChart, #arimaChart, #lstmChart, #comparisonChart { | |
| height: 400px; | |
| } | |
| } | |
| .tab-buttons { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .tab-button { | |
| padding: 8px 16px; | |
| background: #ddd; | |
| color: #333; | |
| border: none; | |
| border-radius: 5px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.3s; | |
| } | |
| .tab-button.active { | |
| background: #667eea; | |
| color: white; | |
| } | |
| .tab-content { | |
| display: none; | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>📊 Castor Price Forecast Dashboard</h1> | |
| <p>Real-time API-powered forecasting with ARIMA & LSTM Models</p> | |
| </div> | |
| <div id="message"></div> | |
| <div class="controls"> | |
| <div class="control-group"> | |
| <label>Start Date</label> | |
| <input type="date" id="startDate" value="2025-12-01"> | |
| </div> | |
| <div class="control-group"> | |
| <label>End Date</label> | |
| <input type="date" id="endDate" value="2025-12-31"> | |
| </div> | |
| <div class="control-group"> | |
| <label>Product</label> | |
| <select id="product"> | |
| <option value="Castor">Castor</option> | |
| </select> | |
| </div> | |
| <div class="button-group"> | |
| <button onclick="loadForecast()">📈 Load Forecast</button> | |
| <button onclick="clearCharts()" style="background: #999;">🔄 Clear</button> | |
| </div> | |
| </div> | |
| <div class="stats" id="stats"></div> | |
| <div class="tab-buttons"> | |
| <button class="tab-button active" onclick="switchTab('combined')">Combined View</button> | |
| <button class="tab-button" onclick="switchTab('arima')">ARIMA Only</button> | |
| <button class="tab-button" onclick="switchTab('lstm')">LSTM Only</button> | |
| <button class="tab-button" onclick="switchTab('comparison')">Model Comparison</button> | |
| </div> | |
| <div class="charts-grid"> | |
| <div id="combined" class="tab-content active"> | |
| <div class="chart-container"> | |
| <div class="chart-title">Combined Forecast (ARIMA + LSTM Average)</div> | |
| <div id="mainChart" class="loading">Load forecast to see chart...</div> | |
| </div> | |
| </div> | |
| <div id="arima" class="tab-content"> | |
| <div class="chart-container"> | |
| <div class="chart-title">ARIMA Model Forecast</div> | |
| <div id="arimaChart" class="loading">Load forecast to see chart...</div> | |
| </div> | |
| </div> | |
| <div id="lstm" class="tab-content"> | |
| <div class="chart-container"> | |
| <div class="chart-title">LSTM Model Forecast</div> | |
| <div id="lstmChart" class="loading">Load forecast to see chart...</div> | |
| </div> | |
| </div> | |
| <div id="comparison" class="tab-content"> | |
| <div class="chart-container"> | |
| <div class="chart-title">Model Comparison</div> | |
| <div id="comparisonChart" class="loading">Load forecast to see chart...</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const API_KEY = 'castor_d167aa169b5e4219a66779e45fbaaefe'; | |
| const API_URL = 'http://127.0.0.1:5000'; | |
| let forecastData = null; | |
| function showMessage(text, type = 'info') { | |
| const msgEl = document.getElementById('message'); | |
| msgEl.className = type; | |
| msgEl.textContent = text; | |
| msgEl.style.display = text ? 'block' : 'none'; | |
| } | |
| async function loadForecast() { | |
| const startDate = document.getElementById('startDate').value; | |
| const endDate = document.getElementById('endDate').value; | |
| const product = document.getElementById('product').value; | |
| if (!startDate || !endDate) { | |
| showMessage('Please select both start and end dates', 'error'); | |
| return; | |
| } | |
| showMessage('Loading forecast data from API...', 'info'); | |
| try { | |
| const url = `${API_URL}/api/forecast?api_key=${API_KEY}&start_date=${startDate}&end_date=${endDate}&product=${product}`; | |
| const response = await fetch(url); | |
| if (!response.ok) { | |
| throw new Error(`API error: ${response.statusText}`); | |
| } | |
| forecastData = await response.json(); | |
| if (forecastData.status !== 'success') { | |
| throw new Error(forecastData.message || 'Unknown error'); | |
| } | |
| showMessage(`✓ Successfully loaded ${forecastData.forecast.length} days of forecast data`, 'success'); | |
| updateCharts(); | |
| updateStats(); | |
| } catch (error) { | |
| showMessage(`Error: ${error.message}. Make sure the API is running at ${API_URL}`, 'error'); | |
| console.error('Fetch error:', error); | |
| } | |
| } | |
| function updateStats() { | |
| if (!forecastData || !forecastData.forecast || forecastData.forecast.length === 0) return; | |
| const forecast = forecastData.forecast; | |
| const prices = forecast.map(d => d.average_price); | |
| const arimaprices = forecast.map(d => d.arima_price); | |
| const lstmPrices = forecast.map(d => d.lstm_price); | |
| const stats = { | |
| last_price: forecastData.last_known_price, | |
| min_price: Math.min(...prices), | |
| max_price: Math.max(...prices), | |
| avg_price: (prices.reduce((a, b) => a + b, 0) / prices.length).toFixed(2), | |
| arima_min: Math.min(...arimaprices).toFixed(2), | |
| arima_max: Math.max(...arimaprices).toFixed(2), | |
| lstm_min: Math.min(...lstmPrices).toFixed(2), | |
| lstm_max: Math.max(...lstmPrices).toFixed(2) | |
| }; | |
| const statsHtml = ` | |
| <div class="stat-card"> | |
| <h3>Current Price</h3> | |
| <div class="value">₹${stats.last_price}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <h3>Forecast Average</h3> | |
| <div class="value">₹${stats.avg_price}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <h3>Forecast Range</h3> | |
| <div class="value">₹${stats.min_price.toFixed(2)} - ₹${stats.max_price.toFixed(2)}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <h3>Days Forecasted</h3> | |
| <div class="value">${forecast.length}</div> | |
| </div> | |
| `; | |
| document.getElementById('stats').innerHTML = statsHtml; | |
| } | |
| function updateCharts() { | |
| if (!forecastData || !forecastData.forecast) return; | |
| const forecast = forecastData.forecast; | |
| const dates = forecast.map(d => d.date); | |
| const avgPrices = forecast.map(d => d.average_price); | |
| const arimaPrices = forecast.map(d => d.arima_price); | |
| const lstmPrices = forecast.map(d => d.lstm_price); | |
| // Combined Chart | |
| const trace1 = { | |
| x: dates, | |
| y: avgPrices, | |
| type: 'scatter', | |
| mode: 'lines+markers', | |
| name: 'Average (ARIMA + LSTM)', | |
| line: { color: '#667eea', width: 3 }, | |
| marker: { size: 4 } | |
| }; | |
| const layout1 = { | |
| title: 'Combined Forecast', | |
| xaxis: { title: 'Date' }, | |
| yaxis: { title: 'Price (₹)' }, | |
| hovermode: 'x unified', | |
| plot_bgcolor: '#f8f9fa', | |
| paper_bgcolor: 'white' | |
| }; | |
| Plotly.newPlot('mainChart', [trace1], layout1, { responsive: true }); | |
| // ARIMA Chart | |
| const trace2 = { | |
| x: dates, | |
| y: arimaPrices, | |
| type: 'scatter', | |
| mode: 'lines+markers', | |
| name: 'ARIMA', | |
| line: { color: '#FF6B6B', width: 3 }, | |
| marker: { size: 4 } | |
| }; | |
| const layout2 = { | |
| title: 'ARIMA Model Forecast', | |
| xaxis: { title: 'Date' }, | |
| yaxis: { title: 'Price (₹)' }, | |
| hovermode: 'x unified', | |
| plot_bgcolor: '#f8f9fa', | |
| paper_bgcolor: 'white' | |
| }; | |
| Plotly.newPlot('arimaChart', [trace2], layout2, { responsive: true }); | |
| // LSTM Chart | |
| const trace3 = { | |
| x: dates, | |
| y: lstmPrices, | |
| type: 'scatter', | |
| mode: 'lines+markers', | |
| name: 'LSTM', | |
| line: { color: '#4ECDC4', width: 3 }, | |
| marker: { size: 4 } | |
| }; | |
| const layout3 = { | |
| title: 'LSTM Model Forecast', | |
| xaxis: { title: 'Date' }, | |
| yaxis: { title: 'Price (₹)' }, | |
| hovermode: 'x unified', | |
| plot_bgcolor: '#f8f9fa', | |
| paper_bgcolor: 'white' | |
| }; | |
| Plotly.newPlot('lstmChart', [trace3], layout3, { responsive: true }); | |
| // Comparison Chart | |
| const layout4 = { | |
| title: 'Model Comparison', | |
| xaxis: { title: 'Date' }, | |
| yaxis: { title: 'Price (₹)' }, | |
| hovermode: 'x unified', | |
| plot_bgcolor: '#f8f9fa', | |
| paper_bgcolor: 'white' | |
| }; | |
| Plotly.newPlot('comparisonChart', [ | |
| { x: dates, y: arimaPrices, type: 'scatter', mode: 'lines', name: 'ARIMA', line: { color: '#FF6B6B', width: 2 } }, | |
| { x: dates, y: lstmPrices, type: 'scatter', mode: 'lines', name: 'LSTM', line: { color: '#4ECDC4', width: 2 } }, | |
| { x: dates, y: avgPrices, type: 'scatter', mode: 'lines', name: 'Average', line: { color: '#667eea', width: 3, dash: 'dash' } } | |
| ], layout4, { responsive: true }); | |
| } | |
| function switchTab(tabName) { | |
| // Hide all tabs | |
| document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active')); | |
| document.querySelectorAll('.tab-button').forEach(el => el.classList.remove('active')); | |
| // Show selected tab | |
| document.getElementById(tabName).classList.add('active'); | |
| event.target.classList.add('active'); | |
| // Redraw charts | |
| setTimeout(() => { | |
| if (tabName === 'combined') Plotly.redraw('mainChart'); | |
| else if (tabName === 'arima') Plotly.redraw('arimaChart'); | |
| else if (tabName === 'lstm') Plotly.redraw('lstmChart'); | |
| else if (tabName === 'comparison') Plotly.redraw('comparisonChart'); | |
| }, 100); | |
| } | |
| function clearCharts() { | |
| forecastData = null; | |
| document.getElementById('stats').innerHTML = ''; | |
| ['mainChart', 'arimaChart', 'lstmChart', 'comparisonChart'].forEach(id => { | |
| document.getElementById(id).innerHTML = '<div class="loading">Load forecast to see chart...</div>'; | |
| }); | |
| showMessage(''); | |
| } | |
| // Load default forecast on page load | |
| window.addEventListener('load', () => { | |
| loadForecast(); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |