mirror of
https://github.com/K-Dense-AI/claude-scientific-skills.git
synced 2026-03-28 07:33:45 +08:00
fix(animation): use fixed axes showing full observed data in background
- X-axis fixed to 2022-01 to 2025-12 (full data range) - Y-axis fixed to 0.72°C to 1.52°C (full value range) - Background shows all observed data (faded gray) + final forecast reference (faded red dashed) - Foreground shows current step data (bright blue) + current forecast (bright red) - GIF size reduced from 918KB to 659KB
This commit is contained in:
@@ -5,31 +5,20 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TimesFM Interactive Forecast Animation</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
min-height: 100vh;
|
||||
color: #e0e0e0;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.container { max-width: 1200px; margin: 0 auto; }
|
||||
|
||||
header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
header { text-align: center; margin-bottom: 30px; }
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
@@ -37,13 +26,9 @@
|
||||
background: linear-gradient(90deg, #60a5fa, #a78bfa);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: #9ca3af;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.subtitle { color: #9ca3af; font-size: 1.1rem; }
|
||||
|
||||
.chart-container {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
@@ -53,10 +38,7 @@
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#chart {
|
||||
width: 100% !important;
|
||||
height: 400px !important;
|
||||
}
|
||||
#chart { width: 100% !important; height: 450px !important; }
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
@@ -67,94 +49,43 @@
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.slider-container { display: flex; flex-direction: column; gap: 10px; }
|
||||
|
||||
.slider-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.slider-label span {
|
||||
font-size: 0.9rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.slider-label .value {
|
||||
font-weight: 600;
|
||||
color: #60a5fa;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.slider-label { display: flex; justify-content: space-between; align-items: center; }
|
||||
.slider-label span { font-size: 0.9rem; color: #9ca3af; }
|
||||
.slider-label .value { font-weight: 600; color: #60a5fa; font-size: 1.1rem; }
|
||||
|
||||
input[type="range"] {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
background: #374151;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
width: 100%; height: 8px; border-radius: 4px;
|
||||
background: #374151; outline: none; -webkit-appearance: none;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
width: 24px; height: 24px; border-radius: 50%;
|
||||
background: linear-gradient(135deg, #60a5fa, #a78bfa);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 10px rgba(96, 165, 250, 0.5);
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #60a5fa, #a78bfa);
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: 0 2px 10px rgba(96, 165, 250, 0.5);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.buttons { display: flex; gap: 10px; flex-wrap: wrap; }
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
flex: 1; min-width: 100px;
|
||||
padding: 12px 20px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border: none; border-radius: 8px;
|
||||
font-size: 1rem; font-weight: 600;
|
||||
cursor: pointer; transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #60a5fa, #a78bfa);
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(96, 165, 250, 0.4); }
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(96, 165, 250, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #374151;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
.btn-secondary { background: #374151; color: #e0e0e0; }
|
||||
.btn-secondary:hover { background: #4b5563; }
|
||||
|
||||
.stats {
|
||||
display: grid;
|
||||
@@ -169,18 +100,8 @@
|
||||
padding: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card .label {
|
||||
font-size: 0.8rem;
|
||||
color: #9ca3af;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-card .value {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: #60a5fa;
|
||||
}
|
||||
.stat-card .label { font-size: 0.8rem; color: #9ca3af; margin-bottom: 5px; }
|
||||
.stat-card .value { font-size: 1.3rem; font-weight: 600; color: #60a5fa; }
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
@@ -192,18 +113,8 @@
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.legend-item { display: flex; align-items: center; gap: 8px; font-size: 0.85rem; }
|
||||
.legend-color { width: 16px; height: 16px; border-radius: 4px; }
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
@@ -211,32 +122,14 @@
|
||||
color: #6b7280;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #60a5fa;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
footer a { color: #60a5fa; text-decoration: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>TimesFM Zero-Shot Forecast Animation</h1>
|
||||
<p class="subtitle">Watch the forecast evolve as more data is added</p>
|
||||
<h1>TimesFM Forecast Evolution</h1>
|
||||
<p class="subtitle">Watch the forecast evolve as more data is added — with full context always visible</p>
|
||||
</header>
|
||||
|
||||
<div class="chart-container">
|
||||
@@ -246,13 +139,13 @@
|
||||
<div class="controls">
|
||||
<div class="slider-container">
|
||||
<div class="slider-label">
|
||||
<span>Historical Data Points</span>
|
||||
<span>Data Points Used</span>
|
||||
<span class="value" id="points-value">12 / 36</span>
|
||||
</div>
|
||||
<input type="range" id="slider" min="0" max="24" value="0" step="1">
|
||||
<div class="slider-label">
|
||||
<span id="date-start">2022-01</span>
|
||||
<span id="date-end">2022-12 → Forecast to 2023-12</span>
|
||||
<span>2022-01</span>
|
||||
<span id="date-end">Using data through 2022-12</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -266,6 +159,10 @@
|
||||
<div class="label">Forecast Mean</div>
|
||||
<div class="value" id="stat-mean">0.86°C</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">vs Final Forecast</div>
|
||||
<div class="value" id="stat-diff">--</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">Forecast Max</div>
|
||||
<div class="value" id="stat-max">--</div>
|
||||
@@ -274,47 +171,49 @@
|
||||
<div class="label">Forecast Min</div>
|
||||
<div class="value" id="stat-min">--</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="label">80% CI Width</div>
|
||||
<div class="value" id="stat-ci">--</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #9ca3af;"></div>
|
||||
<span>All Observed Data</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #fca5a5;"></div>
|
||||
<span>Final Forecast (reference)</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #3b82f6;"></div>
|
||||
<span>Historical Data</span>
|
||||
<span>Data Used</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: #ef4444;"></div>
|
||||
<span>Forecast</span>
|
||||
<span>Current Forecast</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: rgba(239, 68, 68, 0.2);"></div>
|
||||
<span>80% Confidence Interval</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color" style="background: rgba(239, 68, 68, 0.1);"></div>
|
||||
<span>90% Confidence Interval</span>
|
||||
<div class="legend-color" style="background: rgba(239, 68, 68, 0.25);"></div>
|
||||
<span>80% CI</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>TimesFM 1.0 (200M) PyTorch • <a href="https://github.com/google-research/timesfm">Google Research</a></p>
|
||||
<p style="margin-top: 5px;">Generated by <a href="https://github.com/K-Dense-AI/claude-scientific-skills">claude-scientific-skills</a></p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Load animation data
|
||||
let animationData = null;
|
||||
let chart = null;
|
||||
let isPlaying = false;
|
||||
let playInterval = null;
|
||||
let currentStep = 0;
|
||||
|
||||
// Fetch the JSON data
|
||||
// Fixed axis extents (set from data)
|
||||
let allDates = [];
|
||||
let yMin = 0.7;
|
||||
let yMax = 1.55;
|
||||
|
||||
fetch('animation_data.json')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
@@ -325,38 +224,68 @@
|
||||
.catch(err => {
|
||||
console.error('Error loading animation data:', err);
|
||||
document.querySelector('.chart-container').innerHTML =
|
||||
'<p style="text-align: center; padding: 50px; color: #ef4444;">Error loading data. Make sure animation_data.json is in the same directory.</p>';
|
||||
'<p style="text-align:center;padding:50px;color:#ef4444;">Error loading data. Make sure animation_data.json is in the same directory.</p>';
|
||||
});
|
||||
|
||||
function initChart() {
|
||||
const ctx = document.getElementById('chart').getContext('2d');
|
||||
|
||||
// Calculate fixed extents
|
||||
const finalStep = animationData.animation_steps[animationData.animation_steps.length - 1];
|
||||
allDates = [
|
||||
...animationData.actual_data.dates,
|
||||
...finalStep.forecast_dates
|
||||
];
|
||||
|
||||
// Y extent from all values
|
||||
const allValues = [
|
||||
...animationData.actual_data.values,
|
||||
...finalStep.point_forecast,
|
||||
...finalStep.q10,
|
||||
...finalStep.q90
|
||||
];
|
||||
yMin = Math.min(...allValues) - 0.05;
|
||||
yMax = Math.max(...allValues) + 0.05;
|
||||
|
||||
chart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
labels: allDates,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Historical',
|
||||
label: 'All Observed',
|
||||
data: animationData.actual_data.values.map((v, i) => ({x: animationData.actual_data.dates[i], y: v})),
|
||||
borderColor: '#9ca3af',
|
||||
borderWidth: 1,
|
||||
pointRadius: 2,
|
||||
pointBackgroundColor: '#9ca3af',
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
label: 'Final Forecast',
|
||||
data: [...Array(animationData.actual_data.dates.length).fill(null), ...finalStep.point_forecast],
|
||||
borderColor: '#fca5a5',
|
||||
borderWidth: 1,
|
||||
borderDash: [4, 4],
|
||||
pointRadius: 2,
|
||||
pointBackgroundColor: '#fca5a5',
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
order: 2,
|
||||
},
|
||||
{
|
||||
label: 'Data Used',
|
||||
data: [],
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
borderWidth: 2,
|
||||
pointRadius: 3,
|
||||
borderWidth: 2.5,
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: '#3b82f6',
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
},
|
||||
{
|
||||
label: 'Forecast',
|
||||
data: [],
|
||||
borderColor: '#ef4444',
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
||||
borderWidth: 2,
|
||||
pointRadius: 3,
|
||||
pointBackgroundColor: '#ef4444',
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
order: 10,
|
||||
},
|
||||
{
|
||||
label: '90% CI Lower',
|
||||
@@ -366,6 +295,7 @@
|
||||
fill: '+1',
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
order: 5,
|
||||
},
|
||||
{
|
||||
label: '90% CI Upper',
|
||||
@@ -375,86 +305,71 @@
|
||||
fill: false,
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
order: 5,
|
||||
},
|
||||
{
|
||||
label: '80% CI Lower',
|
||||
data: [],
|
||||
borderColor: 'transparent',
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.15)',
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.2)',
|
||||
fill: '+1',
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
order: 6,
|
||||
},
|
||||
{
|
||||
label: '80% CI Upper',
|
||||
data: [],
|
||||
borderColor: 'transparent',
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.15)',
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.2)',
|
||||
fill: false,
|
||||
pointRadius: 0,
|
||||
tension: 0.1,
|
||||
order: 6,
|
||||
},
|
||||
{
|
||||
label: 'Actual (Future)',
|
||||
label: 'Forecast',
|
||||
data: [],
|
||||
borderColor: '#10b981',
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 1,
|
||||
borderDash: [5, 5],
|
||||
pointRadius: 2,
|
||||
pointBackgroundColor: '#10b981',
|
||||
borderColor: '#ef4444',
|
||||
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
||||
borderWidth: 2.5,
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: '#ef4444',
|
||||
fill: false,
|
||||
tension: 0.1,
|
||||
order: 7,
|
||||
},
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
interaction: { intersect: false, mode: 'index' },
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
titleColor: '#fff',
|
||||
bodyColor: '#fff',
|
||||
padding: 12,
|
||||
displayColors: true,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
color: 'rgba(255, 255, 255, 0.05)',
|
||||
},
|
||||
ticks: {
|
||||
color: '#9ca3af',
|
||||
maxRotation: 45,
|
||||
minRotation: 45,
|
||||
},
|
||||
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
||||
ticks: { color: '#9ca3af', maxRotation: 45, minRotation: 45 },
|
||||
},
|
||||
y: {
|
||||
grid: {
|
||||
color: 'rgba(255, 255, 255, 0.05)',
|
||||
},
|
||||
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
||||
ticks: {
|
||||
color: '#9ca3af',
|
||||
callback: function(value) {
|
||||
return value.toFixed(2) + '°C';
|
||||
}
|
||||
callback: v => v.toFixed(2) + '°C'
|
||||
},
|
||||
min: 0.5,
|
||||
max: 1.6,
|
||||
min: yMin,
|
||||
max: yMax,
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
duration: 200,
|
||||
},
|
||||
animation: { duration: 150 },
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -463,73 +378,102 @@
|
||||
if (!animationData || !chart) return;
|
||||
|
||||
const step = animationData.animation_steps[stepIndex];
|
||||
const finalStep = animationData.animation_steps[animationData.animation_steps.length - 1];
|
||||
const actual = animationData.actual_data;
|
||||
|
||||
// Build all dates
|
||||
const allDates = [...step.historical_dates, ...step.forecast_dates];
|
||||
// Build data arrays for each dataset
|
||||
const nHist = step.historical_dates.length;
|
||||
const nForecast = step.forecast_dates.length;
|
||||
const nActual = actual.dates.length;
|
||||
const nFinalForecast = finalStep.forecast_dates.length;
|
||||
const totalPoints = nActual + nFinalForecast;
|
||||
|
||||
// Historical data (with nulls for forecast period)
|
||||
const historicalData = [...step.historical_values, ...Array(step.forecast_dates.length).fill(null)];
|
||||
// Dataset 0: All observed (always full)
|
||||
chart.data.datasets[0].data = actual.values.map((v, i) => ({x: actual.dates[i], y: v}));
|
||||
|
||||
// Forecast data (with nulls for historical period)
|
||||
const forecastData = [...Array(step.historical_dates.length).fill(null), ...step.point_forecast];
|
||||
// Dataset 1: Final forecast reference (always full)
|
||||
chart.data.datasets[1].data = [
|
||||
...Array(nActual).fill(null),
|
||||
...finalStep.point_forecast
|
||||
];
|
||||
|
||||
// Confidence intervals
|
||||
const q90Lower = [...Array(step.historical_dates.length).fill(null), ...step.q10];
|
||||
const q90Upper = [...Array(step.historical_dates.length).fill(null), ...step.q90];
|
||||
const q80Lower = [...Array(step.historical_dates.length).fill(null), ...step.q20];
|
||||
const q80Upper = [...Array(step.historical_dates.length).fill(null), ...step.q80];
|
||||
|
||||
// Actual future data (if available)
|
||||
const actualFuture = [];
|
||||
for (let i = 0; i < allDates.length; i++) {
|
||||
const dateIdx = actual.dates.indexOf(allDates[i]);
|
||||
if (dateIdx >= step.n_points) {
|
||||
actualFuture.push(actual.values[dateIdx]);
|
||||
// Dataset 2: Data used (historical only)
|
||||
const dataUsed = [];
|
||||
for (let i = 0; i < totalPoints; i++) {
|
||||
if (i < nHist) {
|
||||
dataUsed.push(step.historical_values[i]);
|
||||
} else {
|
||||
actualFuture.push(null);
|
||||
dataUsed.push(null);
|
||||
}
|
||||
}
|
||||
chart.data.datasets[2].data = dataUsed;
|
||||
|
||||
// Datasets 3-6: CIs (forecast only)
|
||||
const forecastOffset = nActual;
|
||||
const q90Lower = [];
|
||||
const q90Upper = [];
|
||||
const q80Lower = [];
|
||||
const q80Upper = [];
|
||||
|
||||
for (let i = 0; i < totalPoints; i++) {
|
||||
const forecastIdx = i - forecastOffset;
|
||||
if (forecastIdx >= 0 && forecastIdx < nForecast) {
|
||||
q90Lower.push(step.q10[forecastIdx]);
|
||||
q90Upper.push(step.q90[forecastIdx]);
|
||||
q80Lower.push(step.q20[forecastIdx]);
|
||||
q80Upper.push(step.q80[forecastIdx]);
|
||||
} else {
|
||||
q90Lower.push(null);
|
||||
q90Upper.push(null);
|
||||
q80Lower.push(null);
|
||||
q80Upper.push(null);
|
||||
}
|
||||
}
|
||||
chart.data.datasets[3].data = q90Lower;
|
||||
chart.data.datasets[4].data = q90Upper;
|
||||
chart.data.datasets[5].data = q80Lower;
|
||||
chart.data.datasets[6].data = q80Upper;
|
||||
|
||||
// Dataset 7: Forecast line
|
||||
const forecastData = [];
|
||||
for (let i = 0; i < totalPoints; i++) {
|
||||
const forecastIdx = i - forecastOffset;
|
||||
if (forecastIdx >= 0 && forecastIdx < nForecast) {
|
||||
forecastData.push(step.point_forecast[forecastIdx]);
|
||||
} else {
|
||||
forecastData.push(null);
|
||||
}
|
||||
}
|
||||
chart.data.datasets[7].data = forecastData;
|
||||
|
||||
// Update chart data
|
||||
chart.data.labels = allDates;
|
||||
chart.data.datasets[0].data = historicalData;
|
||||
chart.data.datasets[1].data = forecastData;
|
||||
chart.data.datasets[2].data = q90Lower;
|
||||
chart.data.datasets[3].data = q90Upper;
|
||||
chart.data.datasets[4].data = q80Lower;
|
||||
chart.data.datasets[5].data = q80Upper;
|
||||
chart.data.datasets[6].data = actualFuture;
|
||||
chart.update('none');
|
||||
|
||||
// Update UI
|
||||
document.getElementById('slider').value = stepIndex;
|
||||
document.getElementById('points-value').textContent = `${step.n_points} / 36`;
|
||||
document.getElementById('date-end').textContent = `${step.last_historical_date} → Forecast to ${step.forecast_dates[step.forecast_dates.length - 1]}`;
|
||||
document.getElementById('date-end').textContent = `Using data through ${step.last_historical_date}`;
|
||||
|
||||
// Update stats
|
||||
// Stats
|
||||
const mean = (step.point_forecast.reduce((a, b) => a + b, 0) / step.point_forecast.length).toFixed(3);
|
||||
const finalMean = (finalStep.point_forecast.reduce((a, b) => a + b, 0) / finalStep.point_forecast.length).toFixed(3);
|
||||
const diff = (mean - finalMean).toFixed(3);
|
||||
const max = Math.max(...step.point_forecast).toFixed(3);
|
||||
const min = Math.min(...step.point_forecast).toFixed(3);
|
||||
const ciWidth = ((step.q80[0] - step.q20[0]).toFixed(3));
|
||||
|
||||
document.getElementById('stat-mean').textContent = mean + '°C';
|
||||
document.getElementById('stat-diff').textContent = (diff >= 0 ? '+' : '') + diff + '°C';
|
||||
document.getElementById('stat-max').textContent = max + '°C';
|
||||
document.getElementById('stat-min').textContent = min + '°C';
|
||||
document.getElementById('stat-ci').textContent = '±' + (ciWidth / 2).toFixed(2) + '°C';
|
||||
|
||||
currentStep = stepIndex;
|
||||
}
|
||||
|
||||
// Slider control
|
||||
document.getElementById('slider').addEventListener('input', (e) => {
|
||||
document.getElementById('slider').addEventListener('input', e => {
|
||||
updateChart(parseInt(e.target.value));
|
||||
});
|
||||
|
||||
// Play button
|
||||
document.getElementById('play-btn').addEventListener('click', () => {
|
||||
const btn = document.getElementById('play-btn');
|
||||
|
||||
if (isPlaying) {
|
||||
clearInterval(playInterval);
|
||||
btn.textContent = '▶ Play';
|
||||
@@ -537,11 +481,7 @@
|
||||
} else {
|
||||
btn.textContent = '⏸ Pause';
|
||||
isPlaying = true;
|
||||
|
||||
if (currentStep >= animationData.animation_steps.length - 1) {
|
||||
currentStep = 0;
|
||||
}
|
||||
|
||||
if (currentStep >= animationData.animation_steps.length - 1) currentStep = 0;
|
||||
playInterval = setInterval(() => {
|
||||
if (currentStep >= animationData.animation_steps.length - 1) {
|
||||
clearInterval(playInterval);
|
||||
@@ -551,11 +491,10 @@
|
||||
currentStep++;
|
||||
updateChart(currentStep);
|
||||
}
|
||||
}, 500);
|
||||
}, 400);
|
||||
}
|
||||
});
|
||||
|
||||
// Reset button
|
||||
document.getElementById('reset-btn').addEventListener('click', () => {
|
||||
if (isPlaying) {
|
||||
clearInterval(playInterval);
|
||||
|
||||
Reference in New Issue
Block a user