Skip to content

Commit 899a802

Browse files
committed
Phase 3.1: Add interactive Folium map with quality-based markers
Features: - Interactive geographic visualization of charity sites - Color-coded markers by quality grade (A=green, F=red) - Marker clustering for performance with large datasets - Rich popups with site details, contact info, and recommendations - Filter sites by quality grade (A/B/C/D/F) - Toggle marker clustering on/off - Quality grade legend - Map statistics showing grade distribution - Centers map on average lat/lng of filtered sites Technical: - Uses Folium + streamlit-folium - Integrates with existing quality scoring system - Handles sites without coordinates gracefully - New navigation page: 🗺️ Interactive Map Makes geographic patterns in data quality visible!
1 parent 4bb46ff commit 899a802

1 file changed

Lines changed: 190 additions & 2 deletions

File tree

data_explorer.py

Lines changed: 190 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
- Added comprehensive validation recommendations page
1818
- Implemented quality scoring with actionable improvement suggestions
1919
- Added data quality summary dashboard
20+
21+
Phase 3 Improvements (Interactive Maps & Advanced Features):
22+
- Added Folium interactive maps with quality-based color coding
23+
- Geographic visualization of charity sites
24+
- Cluster markers for performance
25+
- Site detail popups with quality metrics
2026
"""
2127

2228
import os
@@ -62,6 +68,11 @@
6268
import json
6369
from dataclasses import asdict
6470

71+
# Phase 3: Interactive mapping imports
72+
import folium
73+
from streamlit_folium import st_folium
74+
from folium.plugins import MarkerCluster
75+
6576
# Local imports - sys.path setup for module imports
6677
sys.path.append(os.path.join(os.path.dirname(__file__), 'src'))
6778

@@ -1570,8 +1581,181 @@ def display_data_export(sites: List[Dict], organizations: List[Dict]):
15701581
st.dataframe(preview_df[available_cols], use_container_width=True)
15711582

15721583

1584+
# =========================================================================
1585+
# PHASE 3: INTERACTIVE MAPS & ADVANCED FEATURES
1586+
# =========================================================================
1587+
1588+
def display_interactive_map(sites: List[Dict[str, Any]]):
1589+
"""
1590+
Display interactive Folium map of charity sites with quality-based color coding.
1591+
1592+
Phase 3.1: Interactive geographic visualization
1593+
- Color-coded markers by quality grade (A-F)
1594+
- Cluster markers for performance
1595+
- Click markers for site details
1596+
- Filter by quality grade and region
1597+
"""
1598+
from tackle_hunger.data_quality import calculate_site_quality_score, get_quality_grade
1599+
1600+
st.subheader("🗺️ Interactive Site Map")
1601+
st.markdown("*Geographic visualization of charity sites with quality scoring*")
1602+
1603+
if not sites:
1604+
st.info("No sites data available for mapping")
1605+
return
1606+
1607+
# Filter sites with coordinates
1608+
sites_with_coords = [s for s in sites if s.get('lat') and s.get('lng')]
1609+
1610+
if not sites_with_coords:
1611+
st.warning(f"No sites have geographic coordinates. {len(sites)} sites total, 0 with lat/lng.")
1612+
return
1613+
1614+
st.info(f"📍 Showing {len(sites_with_coords):,} sites with coordinates (out of {len(sites):,} total)")
1615+
1616+
# Quality grade filter
1617+
col1, col2 = st.columns(2)
1618+
with col1:
1619+
grade_filter = st.multiselect(
1620+
"Filter by Quality Grade:",
1621+
options=['A', 'B', 'C', 'D', 'F'],
1622+
default=['A', 'B', 'C', 'D', 'F'],
1623+
help="Show only sites with selected quality grades"
1624+
)
1625+
1626+
with col2:
1627+
show_clusters = st.checkbox(
1628+
"Enable Marker Clustering",
1629+
value=True,
1630+
help="Group nearby markers for better performance"
1631+
)
1632+
1633+
# Calculate quality scores for all sites
1634+
sites_with_quality = []
1635+
for site in sites_with_coords:
1636+
quality = calculate_site_quality_score(site)
1637+
if quality['grade'] in grade_filter:
1638+
site_data = site.copy()
1639+
site_data['quality_score'] = quality['overall_score']
1640+
site_data['quality_grade'] = quality['grade']
1641+
site_data['recommendations'] = quality['recommendations']
1642+
sites_with_quality.append(site_data)
1643+
1644+
if not sites_with_quality:
1645+
st.warning("No sites match the selected quality grade filters.")
1646+
return
1647+
1648+
st.success(f"✅ {len(sites_with_quality):,} sites match your filters")
1649+
1650+
# Calculate center of map (average lat/lng)
1651+
avg_lat = sum(s['lat'] for s in sites_with_quality) / len(sites_with_quality)
1652+
avg_lng = sum(s['lng'] for s in sites_with_quality) / len(sites_with_quality)
1653+
1654+
# Create base map
1655+
m = folium.Map(
1656+
location=[avg_lat, avg_lng],
1657+
zoom_start=6,
1658+
tiles='OpenStreetMap'
1659+
)
1660+
1661+
# Define colors for quality grades
1662+
def get_marker_color(grade: str) -> str:
1663+
colors = {
1664+
'A': 'green', # Excellent
1665+
'B': 'blue', # Good
1666+
'C': 'orange', # Fair
1667+
'D': 'cadetblue', # Poor
1668+
'F': 'red' # Critical
1669+
}
1670+
return colors.get(grade, 'gray')
1671+
1672+
# Add markers (with clustering if enabled)
1673+
if show_clusters:
1674+
marker_cluster = MarkerCluster().add_to(m)
1675+
marker_container = marker_cluster
1676+
else:
1677+
marker_container = m
1678+
1679+
for site in sites_with_quality:
1680+
# Create popup HTML with site details
1681+
popup_html = f"""
1682+
<div style="font-family: Arial; width: 250px;">
1683+
<h4 style="margin:0; color: #333;">{site.get('name', 'Unknown Site')}</h4>
1684+
<hr style="margin: 5px 0;">
1685+
<p style="margin: 5px 0;"><strong>Quality Grade:</strong>
1686+
<span style="font-size:18px; font-weight:bold; color: {get_marker_color(site['quality_grade'])};">
1687+
{site['quality_grade']}
1688+
</span>
1689+
({site['quality_score']:.1%})
1690+
</p>
1691+
<p style="margin: 5px 0;"><strong>Address:</strong><br>
1692+
{site.get('street1', 'N/A')}<br>
1693+
{site.get('city', 'N/A')}, {site.get('state', 'N/A')} {site.get('zip', '')}
1694+
</p>
1695+
<p style="margin: 5px 0;"><strong>Contact:</strong><br>
1696+
📞 {site.get('publicPhone') or site.get('phone') or 'N/A'}<br>
1697+
📧 {site.get('publicEmail') or site.get('email') or 'N/A'}<br>
1698+
🌐 {site.get('website', 'N/A')}
1699+
</p>
1700+
<p style="margin: 5px 0; font-size: 11px; color: #666;">
1701+
<strong>Top Recommendations:</strong><br>
1702+
{'<br>'.join(site['recommendations'][:3])}
1703+
</p>
1704+
</div>
1705+
"""
1706+
1707+
# Add marker to map
1708+
folium.Marker(
1709+
location=[site['lat'], site['lng']],
1710+
popup=folium.Popup(popup_html, max_width=300),
1711+
tooltip=f"{site.get('name', 'Unknown')} - Grade: {site['quality_grade']}",
1712+
icon=folium.Icon(
1713+
color=get_marker_color(site['quality_grade']),
1714+
icon='info-sign'
1715+
)
1716+
).add_to(marker_container)
1717+
1718+
# Add legend
1719+
legend_html = '''
1720+
<div style="position: fixed;
1721+
top: 10px; right: 10px; width: 180px; height: 180px;
1722+
background-color: white; border:2px solid grey; z-index:9999;
1723+
font-size:14px; padding: 10px">
1724+
<p style="margin:0; font-weight:bold;">Quality Grade Legend</p>
1725+
<p style="margin:5px 0;"><span style="color:green;">●</span> A - Excellent (90-100%)</p>
1726+
<p style="margin:5px 0;"><span style="color:blue;">●</span> B - Good (80-89%)</p>
1727+
<p style="margin:5px 0;"><span style="color:orange;">●</span> C - Fair (70-79%)</p>
1728+
<p style="margin:5px 0;"><span style="color:cadetblue;">●</span> D - Poor (60-69%)</p>
1729+
<p style="margin:5px 0;"><span style="color:red;">●</span> F - Critical (&lt;60%)</p>
1730+
</div>
1731+
'''
1732+
m.get_root().html.add_child(folium.Element(legend_html))
1733+
1734+
# Display map in Streamlit
1735+
st_folium(m, width=1200, height=600)
1736+
1737+
# Display summary statistics
1738+
st.markdown("---")
1739+
st.subheader("📊 Map Statistics")
1740+
1741+
col1, col2, col3, col4 = st.columns(4)
1742+
1743+
grade_counts = {}
1744+
for grade in ['A', 'B', 'C', 'D', 'F']:
1745+
grade_counts[grade] = len([s for s in sites_with_quality if s['quality_grade'] == grade])
1746+
1747+
with col1:
1748+
st.metric("🟢 A-Grade Sites", grade_counts['A'])
1749+
with col2:
1750+
st.metric("🔵 B-Grade Sites", grade_counts['B'])
1751+
with col3:
1752+
st.metric("🟡 C-Grade Sites", grade_counts['C'])
1753+
with col4:
1754+
st.metric("🔴 D/F-Grade Sites", grade_counts['D'] + grade_counts['F'])
1755+
1756+
15731757
def main():
1574-
"""Main Streamlit application with Phase 1 improvements."""
1758+
"""Main Streamlit application with Phase 1, 2, and 3 improvements."""
15751759
set_page_config()
15761760

15771761
st.title("🍽️ Tackle Hunger Data Explorer")
@@ -1637,7 +1821,8 @@ def main():
16371821
"Quality Analytics",
16381822
"Network Graph",
16391823
"📥 Data Export", # Phase 2: Export functionality
1640-
"✅ Validation Recommendations" # Phase 2: Quality analysis
1824+
"✅ Validation Recommendations", # Phase 2: Quality analysis
1825+
"🗺️ Interactive Map" # Phase 3.1: Interactive map
16411826
]
16421827
)
16431828

@@ -1678,6 +1863,9 @@ def main():
16781863
elif page == "✅ Validation Recommendations":
16791864
# Phase 2: Validation recommendations page
16801865
display_validation_recommendations(sites, organizations)
1866+
elif page == "🗺️ Interactive Map":
1867+
# Phase 3.1: Interactive map
1868+
display_interactive_map(sites)
16811869

16821870
# Footer
16831871
st.markdown("---")

0 commit comments

Comments
 (0)