arima / forecast_dashboard_v2.html
AgentCrafter's picture
first commit
f7ed6f9 verified
Raw
History Blame Contribute Delete
31.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Castor & Soybean 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: #f5f7fa;
padding: 20px;
}
.main-container {
max-width: 1600px;
margin: 0 auto;
}
.header {
background: white;
padding: 30px;
border-radius: 12px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.header h1 {
color: #1a3a52;
font-size: 2em;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 1em;
}
.top-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.left-panel {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
height: fit-content;
}
.commodity-section {
margin-bottom: 25px;
}
.commodity-section label {
display: block;
font-size: 1em;
font-weight: 600;
color: #1a3a52;
margin-bottom: 10px;
}
.commodity-select {
width: 100%;
padding: 12px;
border: 2px solid #e0e4e8;
border-radius: 8px;
font-size: 1em;
background: white;
cursor: pointer;
transition: border-color 0.3s;
}
.commodity-select:hover,
.commodity-select:focus {
border-color: #4CAF50;
outline: none;
}
.time-range {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 25px;
}
.time-btn {
padding: 10px;
border: 2px solid #e0e4e8;
background: white;
border-radius: 8px;
cursor: pointer;
font-size: 0.95em;
font-weight: 600;
color: #666;
transition: all 0.3s;
}
.time-btn:hover {
border-color: #4CAF50;
color: #4CAF50;
}
.time-btn.active {
background: #4CAF50;
color: white;
border-color: #4CAF50;
}
.date-inputs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 15px;
}
.date-input-group {
display: flex;
flex-direction: column;
}
.date-input-group label {
font-size: 0.85em;
font-weight: 600;
color: #666;
margin-bottom: 5px;
}
.date-input-group input {
padding: 10px;
border: 2px solid #e0e4e8;
border-radius: 8px;
font-size: 0.95em;
}
.date-input-group input:focus {
outline: none;
border-color: #4CAF50;
}
.button-group {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
button {
padding: 12px;
border: none;
border-radius: 8px;
font-size: 0.95em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-load {
background: #4CAF50;
color: white;
}
.btn-load:hover {
background: #45a049;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
}
.btn-clear {
background: #f0f0f0;
color: #666;
}
.btn-clear:hover {
background: #e0e0e0;
}
.stats-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
}
.stat-box {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
border-left: 4px solid #4CAF50;
}
.stat-label {
font-size: 0.85em;
color: #666;
margin-bottom: 5px;
}
.stat-value {
font-size: 1.4em;
font-weight: 700;
color: #1a3a52;
}
.right-panel {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.chart-title {
font-size: 1.2em;
font-weight: 600;
color: #1a3a52;
}
.chart-buttons {
display: flex;
gap: 8px;
}
.chart-btn {
padding: 6px 12px;
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 6px;
cursor: pointer;
font-size: 0.9em;
font-weight: 600;
color: #666;
transition: all 0.3s;
}
.chart-btn.active {
background: #4CAF50;
color: white;
border-color: #4CAF50;
}
.chart-btn:hover {
border-color: #4CAF50;
}
.chart-container {
width: 100%;
height: 500px;
border-radius: 8px;
overflow: hidden;
}
#forecastChart {
width: 100% !important;
height: 100% !important;
}
.bottom-section {
background: white;
padding: 25px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-top: 20px;
}
.tab-buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid #e0e4e8;
flex-wrap: wrap;
}
.tab-btn {
padding: 12px 20px;
background: none;
border: none;
border-bottom: 3px solid transparent;
cursor: pointer;
font-size: 0.95em;
font-weight: 600;
color: #666;
transition: all 0.3s;
margin-bottom: -2px;
}
.tab-btn:hover {
color: #4CAF50;
}
.tab-btn.active {
color: #4CAF50;
border-bottom-color: #4CAF50;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.comparison-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
#arimaChart, #lstmChart {
width: 100% !important;
height: 400px !important;
}
.message {
padding: 15px;
border-radius: 8px;
margin-bottom: 15px;
display: none;
}
.message.error {
background: #fee;
color: #c33;
border-left: 4px solid #c33;
display: block;
}
.message.success {
background: #efe;
color: #3c3;
border-left: 4px solid #3c3;
display: block;
}
.loading {
text-align: center;
padding: 40px;
color: #999;
font-size: 1em;
}
@media (max-width: 1200px) {
.top-section {
grid-template-columns: 1fr;
}
.chart-container {
height: 400px;
}
.comparison-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="main-container">
<div class="header">
<h1>📊 Castor & Soybean Price Forecast</h1>
<p>Real-time API-powered forecasting with ARIMA & LSTM Models</p>
</div>
<div id="message" class="message"></div>
<div class="top-section">
<!-- Left Panel: Controls -->
<div class="left-panel">
<div class="commodity-section">
<label>Select Commodity</label>
<select id="product" class="commodity-select">
<option value="Castor">Castor</option>
<option value="Soybean">Soybean</option>
</select>
</div>
<div class="commodity-section">
<label style="display: block; margin-bottom: 10px;">Forecast Period</label>
<div class="time-range">
<button class="time-btn" onclick="setTimeRange(7)">7-Day</button>
<button class="time-btn active" onclick="setTimeRange(15)">15-Day</button>
<button class="time-btn" onclick="setTimeRange(30)">30-Day</button>
</div>
</div>
<div class="commodity-section">
<label>Custom Date Range</label>
<div class="date-inputs">
<div class="date-input-group">
<label style="margin-bottom: 5px; font-size: 0.85em;">From</label>
<input type="date" id="startDate" value="2025-12-01">
</div>
<div class="date-input-group">
<label style="margin-bottom: 5px; font-size: 0.85em;">To</label>
<input type="date" id="endDate" value="2025-12-15">
</div>
</div>
</div>
<div class="button-group">
<button class="btn-load" onclick="loadForecast()">📈 Load Forecast</button>
<button class="btn-clear" onclick="clearCharts()">🔄 Clear</button>
</div>
<div class="commodity-section" style="margin-top: 25px; border-top: 2px solid #e0e4e8; padding-top: 25px;">
<label>Price Statistics</label>
<div class="stats-grid" id="stats">
<div class="stat-box">
<div class="stat-label">Current Price</div>
<div class="stat-value">-</div>
</div>
<div class="stat-box">
<div class="stat-label">Forecast Average</div>
<div class="stat-value">-</div>
</div>
<div class="stat-box">
<div class="stat-label">Min - Max</div>
<div class="stat-value">-</div>
</div>
<div class="stat-box">
<div class="stat-label">Days Forecasted</div>
<div class="stat-value">-</div>
</div>
</div>
</div>
</div>
<!-- Right Panel: Main Chart -->
<div class="right-panel">
<div class="chart-header">
<div class="chart-title">Price Forecast</div>
<div class="chart-buttons">
<button class="chart-btn active" onclick="switchChartType('line')">Line</button>
<button class="chart-btn" onclick="switchChartType('candle')">Candle</button>
</div>
</div>
<div class="chart-container">
<div id="forecastChart" class="loading">Select commodity and click "Load Forecast"</div>
</div>
</div>
</div>
<!-- Bottom Section: Model Comparison -->
<div class="bottom-section">
<div class="tab-buttons">
<button class="tab-btn active" onclick="switchTab('combined')">📊 Combined View</button>
<button class="tab-btn" onclick="switchTab('arima')">📈 ARIMA Model</button>
<button class="tab-btn" onclick="switchTab('lstm')">🧠 LSTM Model</button>
<button class="tab-btn" onclick="switchTab('comparison')">🔄 Model Comparison</button>
</div>
<div id="combined" class="tab-content active">
<div id="combinedChart" class="loading">Load forecast to see chart...</div>
</div>
<div id="arima" class="tab-content">
<div id="arimaChart" class="loading">Load forecast to see chart...</div>
</div>
<div id="lstm" class="tab-content">
<div id="lstmChart" class="loading">Load forecast to see chart...</div>
</div>
<div id="comparison" class="tab-content">
<div id="comparisonChart" class="loading">Load forecast to see chart...</div>
</div>
</div>
</div>
<script>
const API_KEY = 'castor_d167aa169b5e4219a66779e45fbaaefe';
const API_URL = 'http://127.0.0.1:5000';
let forecastData = null;
let chartType = 'line';
function showMessage(text, type = 'info') {
const msgEl = document.getElementById('message');
msgEl.className = 'message';
if (text) msgEl.classList.add(type);
msgEl.textContent = text;
}
function setTimeRange(days) {
// Update button active state
document.querySelectorAll('.time-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
// Calculate dates
const today = new Date('2025-12-01');
const start = today.toISOString().split('T')[0];
const end = new Date(today.getTime() + days * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
document.getElementById('startDate').value = start;
document.getElementById('endDate').value = end;
}
function switchChartType(type) {
chartType = type;
document.querySelectorAll('.chart-btn').forEach(btn => btn.classList.remove('active'));
event.target.classList.add('active');
if (forecastData) {
updateMainChart();
}
}
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');
updateAllCharts();
updateStats();
} catch (error) {
showMessage(`Error: ${error.message}. Ensure API is running at ${API_URL}`, 'error');
console.error('Fetch error:', error);
}
}
function updateStats() {
if (!forecastData || !forecastData.forecast) return;
const forecast = forecastData.forecast;
const prices = forecast.map(d => d.average_price);
const stats = {
current: forecastData.last_known_price,
average: (prices.reduce((a, b) => a + b, 0) / prices.length).toFixed(2),
min: Math.min(...prices).toFixed(2),
max: Math.max(...prices).toFixed(2),
days: forecast.length
};
const statsHtml = `
<div class="stat-box">
<div class="stat-label">Current Price</div>
<div class="stat-value">₹${stats.current}</div>
</div>
<div class="stat-box">
<div class="stat-label">Forecast Average</div>
<div class="stat-value">₹${stats.average}</div>
</div>
<div class="stat-box">
<div class="stat-label">Min - Max</div>
<div class="stat-value">₹${stats.min} - ₹${stats.max}</div>
</div>
<div class="stat-box">
<div class="stat-label">Days Forecasted</div>
<div class="stat-value">${stats.days}</div>
</div>
`;
document.getElementById('stats').innerHTML = statsHtml;
}
function updateMainChart() {
if (!forecastData || !forecastData.forecast) return;
const historical = forecastData.historical || [];
const forecast = forecastData.forecast;
// Combine historical and forecast dates
const historicalDates = historical.map(d => d.date);
const historicalPrices = historical.map(d => d.actual_price);
const forecastDates = 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);
if (chartType === 'line') {
// Historical trace (actual prices)
const traceHistorical = {
x: historicalDates,
y: historicalPrices,
type: 'scatter',
mode: 'lines+markers',
name: 'Actual',
line: { color: '#FF9800', width: 3 },
marker: { size: 4, color: '#FF9800' }
};
// Forecast trace (predicted prices)
const traceForecast = {
x: forecastDates,
y: avgPrices,
type: 'scatter',
mode: 'lines+markers',
name: 'Predicted',
line: { color: '#4CAF50', width: 3 },
marker: { size: 5, color: '#4CAF50' },
fill: 'tozeroy',
fillcolor: 'rgba(76, 175, 80, 0.1)'
};
// Combine all dates for x-axis
const allDates = [...historicalDates, ...forecastDates];
const layout = {
title: 'Price Forecast - Actual vs Predicted',
xaxis: { title: 'Date', gridcolor: '#e0e4e8' },
yaxis: { title: 'Price (₹)', gridcolor: '#e0e4e8' },
hovermode: 'x unified',
plot_bgcolor: 'white',
paper_bgcolor: 'white',
margin: { l: 60, r: 60, t: 50, b: 50 },
shapes: [
{
type: 'line',
x0: historicalDates[historicalDates.length - 1],
x1: historicalDates[historicalDates.length - 1],
y0: 'paper',
y1: 0.95,
line: {
color: '#999',
width: 2,
dash: 'dash'
}
}
]
};
Plotly.newPlot('forecastChart', [traceHistorical, traceForecast], layout, { responsive: true });
} else {
// Candlestick chart with actual and range
const ohlcData = forecast.map((d, i) => ({
open: arimaPrices[i],
high: Math.max(arimaPrices[i], lstmPrices[i]),
low: Math.min(arimaPrices[i], lstmPrices[i]),
close: lstmPrices[i],
date: d.date
}));
const traceCandle = {
x: ohlcData.map(d => d.date),
open: ohlcData.map(d => d.open),
high: ohlcData.map(d => d.high),
low: ohlcData.map(d => d.low),
close: ohlcData.map(d => d.close),
type: 'candlestick',
name: 'Predicted Range'
};
const traceHistorical = {
x: historicalDates,
y: historicalPrices,
type: 'scatter',
mode: 'lines+markers',
name: 'Actual',
line: { color: '#FF9800', width: 2 },
marker: { size: 4, color: '#FF9800' }
};
const layout = {
title: 'Price Forecast - Actual vs Predicted (Candlestick)',
xaxis: { title: 'Date', gridcolor: '#e0e4e8' },
yaxis: { title: 'Price (₹)', gridcolor: '#e0e4e8' },
plot_bgcolor: 'white',
paper_bgcolor: 'white',
margin: { l: 60, r: 60, t: 50, b: 50 }
};
Plotly.newPlot('forecastChart', [traceHistorical, traceCandle], layout, { responsive: true });
}
}
function updateAllCharts() {
updateMainChart();
updateCombinedChart();
updateArimaChart();
updateLstmChart();
updateComparisonChart();
}
function updateCombinedChart() {
if (!forecastData || !forecastData.forecast) return;
const historical = forecastData.historical || [];
const forecast = forecastData.forecast;
const historicalDates = historical.map(d => d.date);
const historicalPrices = historical.map(d => d.actual_price);
const forecastDates = forecast.map(d => d.date);
const avgPrices = forecast.map(d => d.average_price);
const traceHistorical = {
x: historicalDates,
y: historicalPrices,
type: 'scatter',
mode: 'lines+markers',
name: 'Actual',
line: { color: '#FF9800', width: 2 },
marker: { size: 3, color: '#FF9800' }
};
const traceForecast = {
x: forecastDates,
y: avgPrices,
type: 'scatter',
mode: 'lines+markers',
name: 'Predicted',
line: { color: '#4CAF50', width: 2 },
fill: 'tozeroy',
fillcolor: 'rgba(76, 175, 80, 0.1)'
};
const layout = {
title: 'Combined Forecast (Actual + Predicted)',
xaxis: { title: 'Date' },
yaxis: { title: 'Price (₹)' },
plot_bgcolor: 'white',
hovermode: 'x unified'
};
Plotly.newPlot('combinedChart', [traceHistorical, traceForecast], layout, { responsive: true });
}
function updateArimaChart() {
if (!forecastData || !forecastData.forecast) return;
const forecast = forecastData.forecast;
const dates = forecast.map(d => d.date);
const arimaPrices = forecast.map(d => d.arima_price);
const trace = {
x: dates,
y: arimaPrices,
type: 'scatter',
mode: 'lines+markers',
name: 'ARIMA',
line: { color: '#FF6B6B', width: 2 },
marker: { size: 4 }
};
const layout = {
title: 'ARIMA Model Forecast',
xaxis: { title: 'Date' },
yaxis: { title: 'Price (₹)' },
plot_bgcolor: 'white',
hovermode: 'x unified'
};
Plotly.newPlot('arimaChart', [trace], layout, { responsive: true });
}
function updateLstmChart() {
if (!forecastData || !forecastData.forecast) return;
const forecast = forecastData.forecast;
const dates = forecast.map(d => d.date);
const lstmPrices = forecast.map(d => d.lstm_price);
const trace = {
x: dates,
y: lstmPrices,
type: 'scatter',
mode: 'lines+markers',
name: 'LSTM',
line: { color: '#4ECDC4', width: 2 },
marker: { size: 4 }
};
const layout = {
title: 'LSTM Model Forecast',
xaxis: { title: 'Date' },
yaxis: { title: 'Price (₹)' },
plot_bgcolor: 'white',
hovermode: 'x unified'
};
Plotly.newPlot('lstmChart', [trace], layout, { responsive: true });
}
function updateComparisonChart() {
if (!forecastData || !forecastData.forecast) return;
const forecast = forecastData.forecast;
const dates = forecast.map(d => d.date);
const arimaPrices = forecast.map(d => d.arima_price);
const lstmPrices = forecast.map(d => d.lstm_price);
const avgPrices = forecast.map(d => d.average_price);
const traces = [
{ 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: '#4CAF50', width: 3, dash: 'dash' } }
];
const layout = {
title: 'Model Comparison',
xaxis: { title: 'Date' },
yaxis: { title: 'Price (₹)' },
plot_bgcolor: 'white',
hovermode: 'x unified'
};
Plotly.newPlot('comparisonChart', traces, layout, { responsive: true });
}
function switchTab(tabName) {
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(el => el.classList.remove('active'));
document.getElementById(tabName).classList.add('active');
event.target.classList.add('active');
setTimeout(() => {
if (tabName === 'combined') Plotly.redraw('combinedChart');
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 = `
<div class="stat-box"><div class="stat-label">Current Price</div><div class="stat-value">-</div></div>
<div class="stat-box"><div class="stat-label">Forecast Average</div><div class="stat-value">-</div></div>
<div class="stat-box"><div class="stat-label">Min - Max</div><div class="stat-value">-</div></div>
<div class="stat-box"><div class="stat-label">Days Forecasted</div><div class="stat-value">-</div></div>
`;
['forecastChart', 'combinedChart', 'arimaChart', 'lstmChart', 'comparisonChart'].forEach(id => {
document.getElementById(id).innerHTML = '<div class="loading">Load forecast to see chart...</div>';
});
showMessage('');
}
// Initialize on load
window.addEventListener('load', () => {
setTimeRange(15);
});
</script>
</body>
</html>