ojas / forecast_dashboard.html
AgentCrafter's picture
all commits
839f813 verified
Raw
History Blame Contribute Delete
18.3 kB
<!DOCTYPE html>
<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% !important;
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>