Spaces:
Running
Running
Commit
Β·
66a90a7
1
Parent(s):
e86c10c
FIX: Debug interactive plots and add comprehensive loading indicators
Browse filesπ§ Interactive Plot Fixes:
- Fix HTML generation for proper Plotly rendering
- Add comprehensive debugging and fallback displays
- Install Chromium in Docker for PNG export capability
- Improve HTML template structure and error handling
π« Loading Indicators:
- Add animated loading spinners to all buttons
- Implement loading states for upload, download, and analysis
- Add progress text updates during processing
- Improve user feedback across all forms
π UX Improvements:
- Better error messages and debugging info
- Responsive loading animations with CSS keyframes
- Form submission state management
- Enhanced visual feedback for long operations
- Dockerfile +5 -0
- interactive_plot_generator.py +15 -3
- templates/index.html +83 -7
- templates/interactive_plot.html +52 -7
- templates/variables.html +48 -4
Dockerfile
CHANGED
|
@@ -19,6 +19,7 @@ RUN apt-get update && apt-get install -y \
|
|
| 19 |
git-lfs \
|
| 20 |
curl \
|
| 21 |
wget \
|
|
|
|
| 22 |
&& rm -rf /var/lib/apt/lists/*
|
| 23 |
|
| 24 |
# Set environment variables for GDAL
|
|
@@ -26,6 +27,10 @@ ENV CPLUS_INCLUDE_PATH=/usr/include/gdal
|
|
| 26 |
ENV C_INCLUDE_PATH=/usr/include/gdal
|
| 27 |
ENV GDAL_DATA=/usr/share/gdal
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
# Copy requirements first for better caching
|
| 30 |
COPY requirements.txt .
|
| 31 |
|
|
|
|
| 19 |
git-lfs \
|
| 20 |
curl \
|
| 21 |
wget \
|
| 22 |
+
chromium-browser \
|
| 23 |
&& rm -rf /var/lib/apt/lists/*
|
| 24 |
|
| 25 |
# Set environment variables for GDAL
|
|
|
|
| 27 |
ENV C_INCLUDE_PATH=/usr/include/gdal
|
| 28 |
ENV GDAL_DATA=/usr/share/gdal
|
| 29 |
|
| 30 |
+
# Set environment variable for Chromium (needed by Kaleido)
|
| 31 |
+
ENV CHROME_BIN=/usr/bin/chromium-browser
|
| 32 |
+
ENV CHROME_PATH=/usr/bin/chromium-browser
|
| 33 |
+
|
| 34 |
# Copy requirements first for better caching
|
| 35 |
COPY requirements.txt .
|
| 36 |
|
interactive_plot_generator.py
CHANGED
|
@@ -265,19 +265,31 @@ class InteractiveIndiaMapPlotter:
|
|
| 265 |
|
| 266 |
if save_plot:
|
| 267 |
# Generate HTML content for embedding
|
| 268 |
-
html_content = pio.to_html(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
result['html_content'] = html_content
|
| 270 |
|
| 271 |
# Save as HTML file
|
| 272 |
html_path = self._save_html_plot(fig, var_name, display_name, pressure_level, color_theme, time_stamp, config)
|
| 273 |
result['html_path'] = html_path
|
| 274 |
|
| 275 |
-
# Save as PNG for fallback
|
| 276 |
png_path = self._save_png_plot(fig, var_name, display_name, pressure_level, color_theme, time_stamp)
|
| 277 |
result['png_path'] = png_path
|
| 278 |
else:
|
| 279 |
# Just return HTML content for display
|
| 280 |
-
html_content = pio.to_html(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
result['html_content'] = html_content
|
| 282 |
|
| 283 |
return result
|
|
|
|
| 265 |
|
| 266 |
if save_plot:
|
| 267 |
# Generate HTML content for embedding
|
| 268 |
+
html_content = pio.to_html(
|
| 269 |
+
fig,
|
| 270 |
+
config=config,
|
| 271 |
+
include_plotlyjs='cdn',
|
| 272 |
+
div_id='interactive-plot',
|
| 273 |
+
full_html=False
|
| 274 |
+
)
|
| 275 |
result['html_content'] = html_content
|
| 276 |
|
| 277 |
# Save as HTML file
|
| 278 |
html_path = self._save_html_plot(fig, var_name, display_name, pressure_level, color_theme, time_stamp, config)
|
| 279 |
result['html_path'] = html_path
|
| 280 |
|
| 281 |
+
# Save as PNG for fallback (only if kaleido works)
|
| 282 |
png_path = self._save_png_plot(fig, var_name, display_name, pressure_level, color_theme, time_stamp)
|
| 283 |
result['png_path'] = png_path
|
| 284 |
else:
|
| 285 |
# Just return HTML content for display
|
| 286 |
+
html_content = pio.to_html(
|
| 287 |
+
fig,
|
| 288 |
+
config=config,
|
| 289 |
+
include_plotlyjs='cdn',
|
| 290 |
+
div_id='interactive-plot',
|
| 291 |
+
full_html=False
|
| 292 |
+
)
|
| 293 |
result['html_content'] = html_content
|
| 294 |
|
| 295 |
return result
|
templates/index.html
CHANGED
|
@@ -57,13 +57,35 @@
|
|
| 57 |
cursor: pointer;
|
| 58 |
font-size: 16px;
|
| 59 |
font-weight: 600;
|
| 60 |
-
transition:
|
|
|
|
| 61 |
}
|
| 62 |
.btn:hover { background: #2980b9; }
|
| 63 |
.btn:disabled {
|
| 64 |
background: #bdc3c7;
|
| 65 |
cursor: not-allowed;
|
| 66 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
.alert {
|
| 68 |
padding: 15px;
|
| 69 |
margin-bottom: 20px;
|
|
@@ -250,6 +272,59 @@
|
|
| 250 |
</div>
|
| 251 |
|
| 252 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
document.getElementById('file').addEventListener('change', function() {
|
| 254 |
const file = this.files[0];
|
| 255 |
if (file) {
|
|
@@ -261,16 +336,17 @@
|
|
| 261 |
}
|
| 262 |
});
|
| 263 |
|
| 264 |
-
document.querySelector('form[action="/download_date"]').addEventListener('submit', function(e) {
|
| 265 |
-
if (!confirm('CAMS data download may take several minutes. Continue?')) {
|
| 266 |
-
e.preventDefault();
|
| 267 |
-
}
|
| 268 |
-
});
|
| 269 |
-
|
| 270 |
document.getElementById('recentFileForm').addEventListener('submit', function(e) {
|
| 271 |
e.preventDefault();
|
| 272 |
const val = document.getElementById('recent_file').value;
|
| 273 |
if (!val) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
const [filename, filetype] = val.split('|');
|
| 275 |
// is_download param: true for download, false for upload
|
| 276 |
const is_download = (filetype === 'download') ? 'true' : 'false';
|
|
|
|
| 57 |
cursor: pointer;
|
| 58 |
font-size: 16px;
|
| 59 |
font-weight: 600;
|
| 60 |
+
transition: all 0.3s;
|
| 61 |
+
position: relative;
|
| 62 |
}
|
| 63 |
.btn:hover { background: #2980b9; }
|
| 64 |
.btn:disabled {
|
| 65 |
background: #bdc3c7;
|
| 66 |
cursor: not-allowed;
|
| 67 |
}
|
| 68 |
+
.btn.loading {
|
| 69 |
+
background: #34495e;
|
| 70 |
+
cursor: wait;
|
| 71 |
+
padding-left: 50px;
|
| 72 |
+
}
|
| 73 |
+
.btn.loading::before {
|
| 74 |
+
content: "";
|
| 75 |
+
position: absolute;
|
| 76 |
+
left: 15px;
|
| 77 |
+
top: 50%;
|
| 78 |
+
transform: translateY(-50%);
|
| 79 |
+
width: 16px;
|
| 80 |
+
height: 16px;
|
| 81 |
+
border: 2px solid #ffffff40;
|
| 82 |
+
border-top-color: #ffffff;
|
| 83 |
+
border-radius: 50%;
|
| 84 |
+
animation: spin 1s linear infinite;
|
| 85 |
+
}
|
| 86 |
+
@keyframes spin {
|
| 87 |
+
to { transform: translateY(-50%) rotate(360deg); }
|
| 88 |
+
}
|
| 89 |
.alert {
|
| 90 |
padding: 15px;
|
| 91 |
margin-bottom: 20px;
|
|
|
|
| 272 |
</div>
|
| 273 |
|
| 274 |
<script>
|
| 275 |
+
// Loading indicator functionality
|
| 276 |
+
function addLoadingState(button, originalText) {
|
| 277 |
+
button.classList.add('loading');
|
| 278 |
+
button.disabled = true;
|
| 279 |
+
button.textContent = originalText + ' (Processing...)';
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
function removeLoadingState(button, originalText) {
|
| 283 |
+
button.classList.remove('loading');
|
| 284 |
+
button.disabled = false;
|
| 285 |
+
button.textContent = originalText;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
// Add loading states to all form submissions
|
| 289 |
+
document.addEventListener('DOMContentLoaded', function() {
|
| 290 |
+
// File upload form
|
| 291 |
+
const uploadForm = document.querySelector('form[action="/upload"]');
|
| 292 |
+
if (uploadForm) {
|
| 293 |
+
uploadForm.addEventListener('submit', function(e) {
|
| 294 |
+
const submitBtn = this.querySelector('button[type="submit"]');
|
| 295 |
+
if (submitBtn) {
|
| 296 |
+
addLoadingState(submitBtn, 'π Upload File');
|
| 297 |
+
}
|
| 298 |
+
});
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
// Download date form
|
| 302 |
+
const downloadForm = document.querySelector('form[action="/download_date"]');
|
| 303 |
+
if (downloadForm) {
|
| 304 |
+
downloadForm.addEventListener('submit', function(e) {
|
| 305 |
+
if (!confirm('CAMS data download may take several minutes. Continue?')) {
|
| 306 |
+
e.preventDefault();
|
| 307 |
+
return;
|
| 308 |
+
}
|
| 309 |
+
const submitBtn = this.querySelector('button[type="submit"]');
|
| 310 |
+
if (submitBtn) {
|
| 311 |
+
addLoadingState(submitBtn, 'π₯ Download CAMS Data');
|
| 312 |
+
}
|
| 313 |
+
});
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
// Cleanup button
|
| 317 |
+
const cleanupBtn = document.querySelector('a[href="/cleanup"]');
|
| 318 |
+
if (cleanupBtn) {
|
| 319 |
+
cleanupBtn.addEventListener('click', function(e) {
|
| 320 |
+
e.preventDefault();
|
| 321 |
+
this.textContent = 'π§Ή Cleaning...';
|
| 322 |
+
this.style.pointerEvents = 'none';
|
| 323 |
+
window.location.href = this.href;
|
| 324 |
+
});
|
| 325 |
+
}
|
| 326 |
+
});
|
| 327 |
+
|
| 328 |
document.getElementById('file').addEventListener('change', function() {
|
| 329 |
const file = this.files[0];
|
| 330 |
if (file) {
|
|
|
|
| 336 |
}
|
| 337 |
});
|
| 338 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
document.getElementById('recentFileForm').addEventListener('submit', function(e) {
|
| 340 |
e.preventDefault();
|
| 341 |
const val = document.getElementById('recent_file').value;
|
| 342 |
if (!val) return;
|
| 343 |
+
|
| 344 |
+
// Show loading state
|
| 345 |
+
const submitBtn = this.querySelector('button[type="submit"]');
|
| 346 |
+
if (submitBtn) {
|
| 347 |
+
addLoadingState(submitBtn, 'π Analyze');
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
const [filename, filetype] = val.split('|');
|
| 351 |
// is_download param: true for download, false for upload
|
| 352 |
const is_download = (filetype === 'download') ? 'true' : 'false';
|
templates/interactive_plot.html
CHANGED
|
@@ -205,7 +205,7 @@
|
|
| 205 |
</div>
|
| 206 |
|
| 207 |
<div class="plot-container">
|
| 208 |
-
<div class="interactive-plot">
|
| 209 |
{{ plot_html|safe }}
|
| 210 |
</div>
|
| 211 |
</div>
|
|
@@ -269,15 +269,43 @@
|
|
| 269 |
</div>
|
| 270 |
|
| 271 |
<script>
|
| 272 |
-
// Additional interactivity enhancements
|
| 273 |
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
// Add responsive behavior
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
window.addEventListener('resize', function() {
|
| 278 |
Plotly.Plots.resize(plotDiv);
|
| 279 |
-
}
|
| 280 |
-
}
|
| 281 |
|
| 282 |
// Add loading indicator for downloads
|
| 283 |
const downloadButtons = document.querySelectorAll('.btn-download');
|
|
@@ -293,6 +321,23 @@
|
|
| 293 |
}, 2000);
|
| 294 |
});
|
| 295 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
});
|
| 297 |
</script>
|
| 298 |
</body>
|
|
|
|
| 205 |
</div>
|
| 206 |
|
| 207 |
<div class="plot-container">
|
| 208 |
+
<div class="interactive-plot" id="plotly-container">
|
| 209 |
{{ plot_html|safe }}
|
| 210 |
</div>
|
| 211 |
</div>
|
|
|
|
| 269 |
</div>
|
| 270 |
|
| 271 |
<script>
|
| 272 |
+
// Additional interactivity enhancements and debugging
|
| 273 |
document.addEventListener('DOMContentLoaded', function() {
|
| 274 |
+
console.log('Interactive plot page loaded');
|
| 275 |
+
|
| 276 |
+
// Check if Plotly is loaded
|
| 277 |
+
if (typeof Plotly === 'undefined') {
|
| 278 |
+
console.error('Plotly is not loaded!');
|
| 279 |
+
document.getElementById('plotly-container').innerHTML =
|
| 280 |
+
'<div style="padding: 40px; text-align: center; color: red; border: 2px dashed red; margin: 20px;">' +
|
| 281 |
+
'<h3>β οΈ Plot Loading Error</h3>' +
|
| 282 |
+
'<p>Plotly library failed to load. Please refresh the page or check your internet connection.</p>' +
|
| 283 |
+
'</div>';
|
| 284 |
+
return;
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
// Look for the plotly div
|
| 288 |
+
const plotDiv = document.querySelector('[id^="interactive-plot"]') || document.querySelector('.plotly-graph-div');
|
| 289 |
+
|
| 290 |
+
if (!plotDiv) {
|
| 291 |
+
console.error('No Plotly div found!');
|
| 292 |
+
document.getElementById('plotly-container').innerHTML =
|
| 293 |
+
'<div style="padding: 40px; text-align: center; color: orange; border: 2px dashed orange; margin: 20px;">' +
|
| 294 |
+
'<h3>β οΈ Plot Not Found</h3>' +
|
| 295 |
+
'<p>The interactive plot could not be initialized. The data may be processing.</p>' +
|
| 296 |
+
'<button onclick="location.reload()" style="padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 5px; cursor: pointer;">π Refresh Page</button>' +
|
| 297 |
+
'</div>';
|
| 298 |
+
return;
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
console.log('Plotly div found:', plotDiv);
|
| 302 |
+
|
| 303 |
// Add responsive behavior
|
| 304 |
+
window.addEventListener('resize', function() {
|
| 305 |
+
if (plotDiv && typeof Plotly !== 'undefined') {
|
|
|
|
| 306 |
Plotly.Plots.resize(plotDiv);
|
| 307 |
+
}
|
| 308 |
+
});
|
| 309 |
|
| 310 |
// Add loading indicator for downloads
|
| 311 |
const downloadButtons = document.querySelectorAll('.btn-download');
|
|
|
|
| 321 |
}, 2000);
|
| 322 |
});
|
| 323 |
});
|
| 324 |
+
|
| 325 |
+
// Add a small delay then check if plot rendered correctly
|
| 326 |
+
setTimeout(function() {
|
| 327 |
+
const plotlyGraphDiv = document.querySelector('.plotly-graph-div');
|
| 328 |
+
if (plotlyGraphDiv) {
|
| 329 |
+
const svg = plotlyGraphDiv.querySelector('svg');
|
| 330 |
+
if (!svg || svg.children.length === 0) {
|
| 331 |
+
console.warn('Plot may not have rendered correctly');
|
| 332 |
+
// Try to redraw
|
| 333 |
+
if (typeof Plotly !== 'undefined' && plotDiv._fullData) {
|
| 334 |
+
Plotly.redraw(plotDiv);
|
| 335 |
+
}
|
| 336 |
+
} else {
|
| 337 |
+
console.log('Plot rendered successfully');
|
| 338 |
+
}
|
| 339 |
+
}
|
| 340 |
+
}, 1000);
|
| 341 |
});
|
| 342 |
</script>
|
| 343 |
</body>
|
templates/variables.html
CHANGED
|
@@ -57,15 +57,37 @@
|
|
| 57 |
cursor: pointer;
|
| 58 |
font-size: 16px;
|
| 59 |
font-weight: 600;
|
| 60 |
-
transition:
|
| 61 |
text-decoration: none;
|
| 62 |
display: inline-block;
|
|
|
|
| 63 |
}
|
| 64 |
.btn:hover { background: #2980b9; }
|
| 65 |
.btn:disabled {
|
| 66 |
background: #bdc3c7;
|
| 67 |
cursor: not-allowed;
|
| 68 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
.btn-secondary {
|
| 70 |
background: #6c757d;
|
| 71 |
}
|
|
@@ -488,10 +510,32 @@
|
|
| 488 |
return;
|
| 489 |
}
|
| 490 |
|
| 491 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
document.getElementById('loadingMessage').style.display = 'block';
|
| 493 |
-
|
| 494 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 495 |
});
|
| 496 |
|
| 497 |
// Auto-select first variable if only one available
|
|
|
|
| 57 |
cursor: pointer;
|
| 58 |
font-size: 16px;
|
| 59 |
font-weight: 600;
|
| 60 |
+
transition: all 0.3s;
|
| 61 |
text-decoration: none;
|
| 62 |
display: inline-block;
|
| 63 |
+
position: relative;
|
| 64 |
}
|
| 65 |
.btn:hover { background: #2980b9; }
|
| 66 |
.btn:disabled {
|
| 67 |
background: #bdc3c7;
|
| 68 |
cursor: not-allowed;
|
| 69 |
}
|
| 70 |
+
.btn.loading {
|
| 71 |
+
background: #34495e;
|
| 72 |
+
cursor: wait;
|
| 73 |
+
padding-left: 50px;
|
| 74 |
+
}
|
| 75 |
+
.btn.loading::before {
|
| 76 |
+
content: "";
|
| 77 |
+
position: absolute;
|
| 78 |
+
left: 15px;
|
| 79 |
+
top: 50%;
|
| 80 |
+
transform: translateY(-50%);
|
| 81 |
+
width: 16px;
|
| 82 |
+
height: 16px;
|
| 83 |
+
border: 2px solid #ffffff40;
|
| 84 |
+
border-top-color: #ffffff;
|
| 85 |
+
border-radius: 50%;
|
| 86 |
+
animation: spin 1s linear infinite;
|
| 87 |
+
}
|
| 88 |
+
@keyframes spin {
|
| 89 |
+
to { transform: translateY(-50%) rotate(360deg); }
|
| 90 |
+
}
|
| 91 |
.btn-secondary {
|
| 92 |
background: #6c757d;
|
| 93 |
}
|
|
|
|
| 510 |
return;
|
| 511 |
}
|
| 512 |
|
| 513 |
+
// Determine which button was clicked
|
| 514 |
+
const submitEvent = e.submitter;
|
| 515 |
+
let loadingText = 'β³ Generating...';
|
| 516 |
+
|
| 517 |
+
if (submitEvent && submitEvent.formAction) {
|
| 518 |
+
if (submitEvent.formAction.includes('visualize_interactive')) {
|
| 519 |
+
loadingText = 'π― Creating Interactive Plot...';
|
| 520 |
+
} else {
|
| 521 |
+
loadingText = 'π Creating Static Plot...';
|
| 522 |
+
}
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
// Show loading message and update button states
|
| 526 |
document.getElementById('loadingMessage').style.display = 'block';
|
| 527 |
+
|
| 528 |
+
// Update all buttons to loading state
|
| 529 |
+
const buttons = this.querySelectorAll('button[type="submit"]');
|
| 530 |
+
buttons.forEach(btn => {
|
| 531 |
+
btn.disabled = true;
|
| 532 |
+
btn.classList.add('loading');
|
| 533 |
+
if (btn === submitEvent) {
|
| 534 |
+
btn.textContent = loadingText;
|
| 535 |
+
} else {
|
| 536 |
+
btn.style.opacity = '0.5';
|
| 537 |
+
}
|
| 538 |
+
});
|
| 539 |
});
|
| 540 |
|
| 541 |
// Auto-select first variable if only one available
|