Skip to content

Commit 93d5a33

Browse files
authored
Merge pull request #186 from cmu-delphi/alternative_interface
Alternative interface
2 parents e52fe53 + 23f764d commit 93d5a33

File tree

4 files changed

+520
-33
lines changed

4 files changed

+520
-33
lines changed

src/alternative_interface/views.py

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,57 @@
11
from django.shortcuts import render
22
from indicators.models import Indicator
3-
from base.models import Pathogen, Geography
3+
from base.models import Pathogen, GeographyUnit
44

5+
from indicatorsets.utils import group_by_property
6+
import requests
7+
from django.conf import settings
58

69
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."
710

811

12+
def get_available_geos(indicators):
13+
geo_values = []
14+
grouped_indicators = group_by_property(indicators, "data_source")
15+
for data_source, indicators in grouped_indicators.items():
16+
indicators_str = ",".join(indicator["name"] for indicator in indicators)
17+
response = requests.get(
18+
f"{settings.EPIDATA_URL}covidcast/geo_indicator_coverage",
19+
params={"data_source": data_source, "signals": indicators_str},
20+
auth=("epidata", settings.EPIDATA_API_KEY),
21+
)
22+
if response.status_code == 200:
23+
data = response.json()
24+
if len(data["epidata"]):
25+
geo_values.extend(data["epidata"])
26+
unique_values = set(geo_values)
27+
geo_levels = set([el.split(":")[0] for el in unique_values])
28+
geo_unit_ids = set([geo_value.split(":")[1] for geo_value in unique_values])
29+
geographic_granularities = [
30+
{
31+
"id": f"{geo_unit.geo_level.name}:{geo_unit.geo_id}",
32+
"geoType": geo_unit.geo_level.name,
33+
"text": geo_unit.display_name,
34+
"geoTypeDisplayName": geo_unit.geo_level.display_name,
35+
}
36+
for geo_unit in GeographyUnit.objects.filter(geo_level__name__in=geo_levels)
37+
.filter(geo_id__in=geo_unit_ids)
38+
.prefetch_related("geo_level")
39+
.order_by("level")
40+
]
41+
grouped_geographic_granularities = group_by_property(
42+
geographic_granularities, "geoTypeDisplayName"
43+
)
44+
geographic_granularities = []
45+
for key, value in grouped_geographic_granularities.items():
46+
geographic_granularities.append(
47+
{
48+
"text": key,
49+
"children": value,
50+
}
51+
)
52+
return geographic_granularities
53+
54+
955
def alternative_interface_view(request):
1056
try:
1157
ctx = {}
@@ -18,42 +64,33 @@ def alternative_interface_view(request):
1864
)
1965
)
2066

21-
# Fetch geographies for dropdown
22-
ctx["geographies"] = list(
23-
Geography.objects.filter(used_in="indicatorsets").order_by(
24-
"display_order_number"
25-
)
26-
)
27-
2867
# Get filters from URL parameters
2968
pathogen_filter = request.GET.get("pathogen", "")
3069
geography_filter = request.GET.get("geography", "")
3170
ctx["selected_pathogen"] = pathogen_filter
3271
ctx["selected_geography"] = geography_filter
3372

3473
# Build queryset with optional filtering
35-
indicators_qs = Indicator.objects.prefetch_related("pathogens", "available_geographies").all()
74+
indicators_qs = Indicator.objects.prefetch_related(
75+
"pathogens", "available_geographies", "indicator_set"
76+
).all()
3677

3778
if pathogen_filter:
38-
indicators_qs = indicators_qs.filter(pathogens__id=pathogen_filter)
39-
40-
if geography_filter:
41-
indicators_qs = indicators_qs.filter(available_geographies__id=geography_filter)
79+
indicators_qs = indicators_qs.filter(pathogens__id=pathogen_filter, indicator_set__epidata_endpoint="covidcast")
4280

4381
# Convert to list of dictionaries
4482
ctx["indicators"] = [
4583
{
46-
"id": indicator.id,
84+
"_endpoint": indicator.indicator_set.epidata_endpoint,
4785
"name": indicator.name,
48-
"source": indicator.source.name if indicator.source else "Unknown",
49-
"geographic_scope": indicator.geographic_scope.name if indicator.geographic_scope else "Unknown",
50-
"temporal_scope_end": indicator.temporal_scope_end,
51-
"description": indicator.description,
52-
"pathogens": [pathogen.name for pathogen in indicator.pathogens.all()],
86+
"data_source": indicator.source.name if indicator.source else "Unknown",
87+
"time_type": indicator.time_type,
5388
}
5489
for indicator in indicators_qs
5590
]
5691

92+
ctx["available_geos"] = get_available_geos(ctx["indicators"])
93+
5794
return render(
5895
request, "alternative_interface/alter_dashboard.html", context=ctx
5996
)

src/assets/css/alter_dashboard.css

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,39 @@
2424
box-sizing: border-box;
2525
}
2626

27+
/* Page Loader */
28+
.page-loader {
29+
position: fixed;
30+
top: 0;
31+
left: 0;
32+
width: 100%;
33+
height: 100%;
34+
background-color: rgba(255, 255, 255, 0.95);
35+
display: flex;
36+
justify-content: center;
37+
align-items: center;
38+
z-index: 9999;
39+
backdrop-filter: blur(4px);
40+
}
41+
42+
.loader-content {
43+
text-align: center;
44+
}
45+
46+
.loader-content .spinner-border {
47+
width: 3rem;
48+
height: 3rem;
49+
border-width: 0.3rem;
50+
margin-bottom: 1rem;
51+
}
52+
53+
.loader-text {
54+
color: var(--dark-text);
55+
font-size: 1rem;
56+
font-weight: 500;
57+
margin-top: 1rem;
58+
}
59+
2760
body {
2861
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2962
background-color: var(--light-bg);
@@ -181,6 +214,83 @@ body {
181214
max-height: 100%;
182215
}
183216

217+
/* Chart Controls */
218+
.chart-controls {
219+
margin-top: 1rem;
220+
padding-top: 1rem;
221+
border-top: 1px solid var(--border-color);
222+
}
223+
224+
.controls-group {
225+
display: flex;
226+
gap: 0.75rem;
227+
flex-wrap: wrap;
228+
align-items: center;
229+
}
230+
231+
.btn-control {
232+
display: inline-flex;
233+
align-items: center;
234+
gap: 0.5rem;
235+
padding: 0.5rem 1rem;
236+
background: var(--light-bg);
237+
border: 1px solid var(--border-color);
238+
border-radius: 6px;
239+
color: var(--dark-text);
240+
font-size: 0.875rem;
241+
font-weight: 500;
242+
cursor: pointer;
243+
transition: var(--transition);
244+
white-space: nowrap;
245+
}
246+
247+
.btn-control:hover {
248+
background: #f1f5f9;
249+
border-color: var(--primary-color);
250+
color: var(--primary-color);
251+
transform: translateY(-1px);
252+
box-shadow: var(--shadow-sm);
253+
}
254+
255+
.btn-control.active {
256+
background: var(--primary-color);
257+
border-color: var(--primary-color);
258+
color: white;
259+
}
260+
261+
.btn-control.active:hover {
262+
background: var(--primary-dark);
263+
border-color: var(--primary-dark);
264+
color: white;
265+
}
266+
267+
.control-icon {
268+
font-size: 1rem;
269+
line-height: 1;
270+
}
271+
272+
.control-text {
273+
line-height: 1;
274+
}
275+
276+
/* Enhanced Legend Styling */
277+
canvas + div {
278+
margin-top: 1rem;
279+
}
280+
281+
/* Responsive Controls */
282+
@media (max-width: 768px) {
283+
.controls-group {
284+
flex-direction: column;
285+
align-items: stretch;
286+
}
287+
288+
.btn-control {
289+
width: 100%;
290+
justify-content: center;
291+
}
292+
}
293+
184294
/* Footer */
185295
footer {
186296
background: white;

0 commit comments

Comments
 (0)