From af5ad225026869cd779053334082b254da2bacaf Mon Sep 17 00:00:00 2001 From: Dmytro Trotsko Date: Tue, 28 Oct 2025 00:29:57 +0200 Subject: [PATCH] Removed indicators table, added chart mockup --- src/alternative_interface/views.py | 20 +- src/assets/css/alter_dashboard.css | 401 ++++-------------- src/assets/js/alter_dashboard.js | 296 +++++++------ .../alter_dashboard.html | 85 ++-- 4 files changed, 296 insertions(+), 506 deletions(-) diff --git a/src/alternative_interface/views.py b/src/alternative_interface/views.py index 9286a64..830da55 100644 --- a/src/alternative_interface/views.py +++ b/src/alternative_interface/views.py @@ -1,6 +1,6 @@ from django.shortcuts import render from indicators.models import Indicator -from base.models import Pathogen +from base.models import Pathogen, Geography HEADER_DESCRIPTION = "Discover, display and download real-time infectious disease indicators (time series) that track a variety of pathogens, diseases and syndromes in a variety of locations (primarily within the USA). Browse the list, or filter it first by locations and pathogens of interest, by surveillance categories, and more. Expand any row to expose and select from a set of related indicators, then hit 'Show Selected Indicators' at bottom to plot or export your selected indicators, or to generate code snippets to retrieve them from the Delphi Epidata API. Most indicators are served from the Delphi Epidata real-time repository, but some may be available only from third parties or may require prior approval." @@ -18,16 +18,28 @@ def alternative_interface_view(request): ) ) - # Get pathogen filter from URL parameters + # Fetch geographies for dropdown + ctx["geographies"] = list( + Geography.objects.filter(used_in="indicatorsets").order_by( + "display_order_number" + ) + ) + + # Get filters from URL parameters pathogen_filter = request.GET.get("pathogen", "") + geography_filter = request.GET.get("geography", "") ctx["selected_pathogen"] = pathogen_filter + ctx["selected_geography"] = geography_filter - # Build queryset with optional pathogen filtering - indicators_qs = Indicator.objects.prefetch_related("pathogens").all() + # Build queryset with optional filtering + indicators_qs = Indicator.objects.prefetch_related("pathogens", "available_geographies").all() if pathogen_filter: indicators_qs = indicators_qs.filter(pathogens__id=pathogen_filter) + if geography_filter: + indicators_qs = indicators_qs.filter(available_geographies__id=geography_filter) + # Convert to list of dictionaries ctx["indicators"] = [ { diff --git a/src/assets/css/alter_dashboard.css b/src/assets/css/alter_dashboard.css index c7a4536..9784c3c 100644 --- a/src/assets/css/alter_dashboard.css +++ b/src/assets/css/alter_dashboard.css @@ -78,33 +78,52 @@ body { z-index: 1; } -/* Hero Controls */ -.hero-controls { - background: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(10px); - border-radius: var(--border-radius); - padding: 1.5rem; - border: 1px solid rgba(255, 255, 255, 0.2); +/* Filter Hero Section */ +.filter-hero-section { + background: linear-gradient(135deg, #475569 0%, #334155 100%); + padding: 2rem 0; + margin-bottom: 3rem; + position: relative; + overflow: hidden; +} + +.filter-hero-content { + position: relative; + z-index: 1; +} + +/* Filter Form Inline */ +.filter-form-inline { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: nowrap; + justify-content: center; + white-space: nowrap; } -.hero-controls .form-label { +.filter-label { + color: rgba(255, 255, 255, 0.9); font-weight: 500; - font-size: 0.875rem; - text-transform: uppercase; - letter-spacing: 0.05em; + font-size: 1rem; + white-space: nowrap; } -.hero-controls .form-select { - background-color: rgba(255, 255, 255, 0.9); +.filter-select { + min-width: 220px; + padding: 0.75rem 1rem; border: 1px solid rgba(255, 255, 255, 0.3); - color: var(--dark-text); + border-radius: var(--border-radius); + background-color: rgba(255, 255, 255, 0.95); font-weight: 500; + transition: var(--transition); } -.hero-controls .form-select:focus { +.filter-select:focus { background-color: white; border-color: rgba(255, 255, 255, 0.5); box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.2); + outline: none; } @@ -122,21 +141,7 @@ body { font-weight: 400; } -/* Card Enhancements */ -.card { - border: none; - border-radius: var(--border-radius); - box-shadow: var(--shadow-sm); - transition: var(--transition); - background: white; - overflow: hidden; -} - -.card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-lg); -} - +/* Card Header */ .card-header { background: white; border-bottom: 1px solid var(--border-color); @@ -151,224 +156,31 @@ body { color: var(--dark-text); } -.card-body { - padding: 1.5rem; -} - -/* Chart Container */ -.chart-container { - position: relative; - height: 300px; - padding: 1rem; -} - -.chart-container canvas { - max-height: 100%; -} - -/* Stats Grid */ -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1.5rem; - margin-bottom: 3rem; -} - -.stat-card { - background: white; - padding: 2rem; - border-radius: var(--border-radius); - box-shadow: var(--shadow-sm); - text-align: center; - transition: var(--transition); - border: 1px solid var(--border-color); -} - -.stat-card:hover { - transform: translateY(-2px); - box-shadow: var(--shadow-md); -} - -.stat-number { - font-size: 2.5rem; - font-weight: 700; - color: var(--primary-color); - margin-bottom: 0.5rem; - line-height: 1; -} - -.stat-label { - color: var(--secondary-color); - font-weight: 500; - font-size: 0.875rem; - text-transform: uppercase; - letter-spacing: 0.05em; -} - -/* Filter Section */ -.filter-section { - background: white; - padding: 2rem; - border-radius: var(--border-radius); - margin-bottom: 2rem; - box-shadow: var(--shadow-sm); - border: 1px solid var(--border-color); -} - -.filter-title { - font-weight: 600; - margin-bottom: 1rem; - color: var(--dark-text); - font-size: 1.125rem; -} - -.form-label { - font-weight: 500; - color: var(--dark-text); - margin-bottom: 0.5rem; -} - -.form-select { - border: 1px solid var(--border-color); - border-radius: 8px; - padding: 0.75rem; - font-size: 0.875rem; - transition: var(--transition); -} - -.form-select:focus { - border-color: var(--primary-color); - box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); -} - -/* Button Styles */ -.btn { - border-radius: 8px; - font-weight: 500; - padding: 0.75rem 1.5rem; - font-size: 0.875rem; - transition: var(--transition); - border: none; -} - -.btn-primary { - background-color: var(--primary-color); - color: white; -} - -.btn-primary:hover { - background-color: var(--primary-dark); - transform: translateY(-1px); -} - -.btn-outline-secondary { - border: 1px solid var(--border-color); - color: var(--secondary-color); -} - -.btn-outline-secondary:hover { - background-color: var(--light-bg); - border-color: var(--secondary-color); -} - -.btn-sm { - padding: 0.5rem 1rem; - font-size: 0.75rem; -} - -/* Table Styles */ -.table-container { +/* Chart Section */ +.chart-section { background: white; border-radius: var(--border-radius); + margin-top: 3rem; overflow: hidden; box-shadow: var(--shadow-sm); border: 1px solid var(--border-color); } -.table { - margin: 0; - font-size: 0.875rem; -} - -.table th { - background-color: var(--light-bg); - border: none; - padding: 1rem; - font-weight: 600; - color: var(--dark-text); - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.05em; +.chart-container-wrapper { + padding: 1.5rem; } -.table td { - border: none; +/* Chart Container */ +.chart-container { + position: relative; + height: 400px; padding: 1rem; - border-bottom: 1px solid var(--border-color); - vertical-align: middle; -} - -.table tbody tr:hover { - background-color: rgba(37, 99, 235, 0.05); } -/* Badge Styles */ -.badge { - font-size: 0.75rem; - padding: 0.5rem 0.75rem; - border-radius: 6px; - font-weight: 500; -} - -.bg-primary { - background-color: var(--primary-color) !important; -} - -.bg-secondary { - background-color: var(--secondary-color) !important; -} - -.bg-success { - background-color: var(--success-color) !important; -} - -.bg-warning { - background-color: var(--warning-color) !important; -} - -.bg-danger { - background-color: var(--danger-color) !important; -} - -/* Data Source Badges */ -.data-source-badge { - background-color: var(--light-bg); - color: var(--secondary-color); - padding: 0.25rem 0.75rem; - border-radius: 6px; - font-size: 0.75rem; - margin-right: 0.5rem; - margin-bottom: 0.25rem; - display: inline-block; - font-weight: 500; -} - -/* Loading Spinner */ -.loading-spinner { - display: inline-block; - width: 20px; - height: 20px; - border: 3px solid rgba(37, 99, 235, 0.3); - border-radius: 50%; - border-top-color: var(--primary-color); - animation: spin 1s ease-in-out infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } +.chart-container canvas { + max-height: 100%; } - /* Footer */ footer { background: white; @@ -387,41 +199,41 @@ footer { font-size: 1rem; } - .hero-controls { - margin-top: 2rem; - padding: 1rem; + .filter-hero-section { + padding: 1.5rem 0; + margin-bottom: 2rem; } - - .stats-grid { - grid-template-columns: 1fr; - gap: 1rem; + .filter-form-inline { + flex-wrap: wrap; + flex-direction: column; + align-items: stretch; + gap: 0.75rem; } - .stat-card { - padding: 1.5rem; + .filter-select { + width: 100%; + min-width: auto; } - .stat-number { - font-size: 2rem; + .filter-label { + text-align: center; } - .filter-section { - padding: 1.5rem; + .card-header { + padding: 1rem; } - .card-header, - .card-body { - padding: 1rem; + .chart-section { + margin-top: 2rem; } .chart-container { - height: 250px; + height: 300px; } - .table th, - .table td { - padding: 0.75rem 0.5rem; + .chart-container-wrapper { + padding: 1rem; } } @@ -438,71 +250,26 @@ footer { font-size: 0.875rem; } - .stat-number { - font-size: 1.75rem; + .filter-hero-section { + padding: 1rem 0; + margin-bottom: 1.5rem; } - .btn { - padding: 0.625rem 1.25rem; - font-size: 0.75rem; + .filter-label { + font-size: 0.875rem; + } + + .filter-select { + padding: 0.625rem 0.875rem; + font-size: 0.875rem; + } + + .chart-section { + margin-top: 1.5rem; + } + + .chart-container { + height: 250px; } } -/* Animation Classes */ -.fade-in { - animation: fadeIn 0.5s ease-in; -} - -@keyframes fadeIn { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } -} - -.slide-up { - animation: slideUp 0.3s ease-out; -} - -@keyframes slideUp { - from { transform: translateY(10px); opacity: 0; } - to { transform: translateY(0); opacity: 1; } -} - -/* Utility Classes */ -.text-muted { - color: var(--secondary-color) !important; -} - -.border-0 { - border: 0 !important; -} - -.shadow-none { - box-shadow: none !important; -} - -/* Chart.js Customizations */ -.chartjs-tooltip { - background-color: rgba(0, 0, 0, 0.8) !important; - border-radius: 6px !important; - padding: 0.5rem !important; - font-size: 0.75rem !important; -} - -/* Custom Scrollbar */ -.table-responsive::-webkit-scrollbar { - height: 8px; -} - -.table-responsive::-webkit-scrollbar-track { - background: var(--light-bg); - border-radius: 4px; -} - -.table-responsive::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 4px; -} - -.table-responsive::-webkit-scrollbar-thumb:hover { - background: var(--secondary-color); -} diff --git a/src/assets/js/alter_dashboard.js b/src/assets/js/alter_dashboard.js index 128a104..bb9fcde 100644 --- a/src/assets/js/alter_dashboard.js +++ b/src/assets/js/alter_dashboard.js @@ -9,144 +9,177 @@ class AlterDashboard { } init() { - this.setupEventListeners(); - this.loadInitialData(); + this.initChart(); } - loadInitialData() { - // All data is already loaded in the template - this.renderTable(window.djangoIndicators || []); - } - - - setupEventListeners() { - // No event listeners needed for filtering - handled by Django - } - - - renderTable(indicators) { - const tbody = document.getElementById('indicatorsTableBody'); - if (!tbody) return; - - if (indicators.length === 0) { - tbody.innerHTML = ` - - - No indicators found matching your criteria. - - - `; - return; - } - - // Use DocumentFragment for better performance - const fragment = document.createDocumentFragment(); - - indicators.forEach(indicator => { - const row = document.createElement('tr'); - row.className = 'fade-in'; - row.innerHTML = ` - ${this.escapeHtml(indicator.name)} - ${this.renderPathogens(indicator.pathogens)} - ${this.escapeHtml(indicator.description || '')} - ${this.escapeHtml(indicator.temporal_scope_end || '')} - - - - `; - fragment.appendChild(row); + initChart() { + const ctx = document.getElementById('indicatorChart'); + if (!ctx) return; + + // Sample data for the chart - similar to Delphi EpiVis + const labels = this.generateDateLabels(30); + const datasets = [ + { + label: 'Claims hosp (COVID:down-adjusted)', + data: this.generateSampleData(30, 100), + borderColor: '#0076aa', + backgroundColor: 'rgba(0, 118, 170, 0.1)', + borderWidth: 2, + fill: true, + tension: 0.4, + pointRadius: 0, + pointHoverRadius: 4 + }, + { + label: 'Claims hosp (COVID:)', + data: this.generateSampleData(30, 120), + borderColor: '#5489a2', + backgroundColor: 'rgba(84, 137, 162, 0.1)', + borderWidth: 2, + fill: true, + tension: 0.4, + pointRadius: 0, + pointHoverRadius: 4 + }, + { + label: 'Claims OV (COVID-related:down-adjusted)', + data: this.generateSampleData(30, 90), + borderColor: '#de1dbb', + backgroundColor: 'rgba(222, 29, 187, 0.1)', + borderWidth: 2, + fill: true, + tension: 0.4, + pointRadius: 0, + pointHoverRadius: 4 + }, + { + label: 'Claims OV (COVID-related:)', + data: this.generateSampleData(30, 110), + borderColor: '#a67c83', + backgroundColor: 'rgba(166, 124, 131, 0.1)', + borderWidth: 2, + fill: true, + tension: 0.4, + pointRadius: 0, + pointHoverRadius: 4 + } + ]; + + this.chart = new Chart(ctx, { + type: 'line', + data: { + labels: labels, + datasets: datasets + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'top', + labels: { + usePointStyle: true, + padding: 15, + font: { + size: 12, + weight: '500' + } + } + }, + tooltip: { + mode: 'index', + intersect: false, + backgroundColor: 'rgba(0, 0, 0, 0.8)', + padding: 12, + titleFont: { + size: 14, + weight: '600' + }, + bodyFont: { + size: 12 + }, + displayColors: true, + callbacks: { + title: function(context) { + return context[0].label; + }, + label: function(context) { + return context.dataset.label + ': ' + context.parsed.y.toFixed(2); + } + } + } + }, + scales: { + x: { + display: true, + grid: { + display: false, + drawBorder: false + }, + ticks: { + font: { + size: 11 + }, + color: '#64748b', + maxTicksLimit: 8 + } + }, + y: { + display: true, + beginAtZero: false, + grid: { + color: 'rgba(226, 232, 240, 0.8)', + drawBorder: false + }, + ticks: { + font: { + size: 11 + }, + color: '#64748b', + callback: function(value) { + return value.toFixed(1); + } + } + } + }, + interaction: { + mode: 'nearest', + axis: 'x', + intersect: false + }, + animation: { + duration: 1000, + easing: 'easeInOutQuart' + } + } }); - - tbody.innerHTML = ''; - tbody.appendChild(fragment); } - renderPathogens(pathogens) { - if (!pathogens || pathogens.length === 0) { - return 'None'; + generateDateLabels(days) { + const labels = []; + const today = new Date(); + for (let i = days - 1; i >= 0; i--) { + const date = new Date(today); + date.setDate(date.getDate() - i); + const month = (date.getMonth() + 1).toString().padStart(2, '0'); + const day = date.getDate().toString().padStart(2, '0'); + labels.push(`${month}/${day}`); } - - return pathogens.map(pathogen => - `${this.escapeHtml(pathogen)}` - ).join(''); + return labels; } - - viewIndicator(indicatorId) { - // Find indicator in the original data - const allIndicators = window.djangoIndicators || []; - const indicator = allIndicators.find(i => i.id === indicatorId); - - if (!indicator) { - console.error('Indicator not found:', indicatorId); - return; + generateSampleData(count, startValue = 100) { + const data = []; + let baseValue = startValue; + for (let i = 0; i < count; i++) { + // Generate realistic-looking data with trend and noise + const trend = Math.sin(i / count * Math.PI * 2) * 10; + const noise = (Math.random() - 0.5) * 15; + baseValue += trend / count + noise; + data.push(Math.max(50, Math.round(baseValue))); } - - // Create detailed view modal - const modal = document.createElement('div'); - modal.className = 'modal fade'; - modal.innerHTML = ` - - `; - - document.body.appendChild(modal); - const bsModal = new bootstrap.Modal(modal); - bsModal.show(); - - modal.addEventListener('hidden.bs.modal', () => { - document.body.removeChild(modal); - }); + return data; } escapeHtml(text) { @@ -156,11 +189,6 @@ class AlterDashboard { } } -// Global functions for template compatibility -function viewIndicator(indicatorId) { - dashboard.viewIndicator(indicatorId); -} - // Initialize dashboard when DOM is loaded let dashboard; document.addEventListener('DOMContentLoaded', function() { diff --git a/src/templates/alternative_interface/alter_dashboard.html b/src/templates/alternative_interface/alter_dashboard.html index 6553a63..6d31bf0 100644 --- a/src/templates/alternative_interface/alter_dashboard.html +++ b/src/templates/alternative_interface/alter_dashboard.html @@ -42,73 +42,56 @@
-
+

Respiratory Diseases Dashboard

Track COVID-19, influenza, and RSV activity across the United States with real-time data from multiple sources.

-
-
-
-
- - -
-
-
-
-
- -
-
-
{{ indicators|length }}
-
Total Indicators
+ +
+
+
+
+ Show + + in + +
+
- - -
+
+ +
-
Available Indicators
-

Explore detailed information about each indicator

-
-
- - - - - - - - - - - - - -
NamePathogensDescriptionTemporal Scope EndActions
+
Indicator Visualization
+

Visualize trends and patterns

- - -