|
17 | 17 | - Added comprehensive validation recommendations page |
18 | 18 | - Implemented quality scoring with actionable improvement suggestions |
19 | 19 | - 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 |
20 | 26 | """ |
21 | 27 |
|
22 | 28 | import os |
|
62 | 68 | import json |
63 | 69 | from dataclasses import asdict |
64 | 70 |
|
| 71 | +# Phase 3: Interactive mapping imports |
| 72 | +import folium |
| 73 | +from streamlit_folium import st_folium |
| 74 | +from folium.plugins import MarkerCluster |
| 75 | + |
65 | 76 | # Local imports - sys.path setup for module imports |
66 | 77 | sys.path.append(os.path.join(os.path.dirname(__file__), 'src')) |
67 | 78 |
|
@@ -1570,8 +1581,181 @@ def display_data_export(sites: List[Dict], organizations: List[Dict]): |
1570 | 1581 | st.dataframe(preview_df[available_cols], use_container_width=True) |
1571 | 1582 |
|
1572 | 1583 |
|
| 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 (<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 | + |
1573 | 1757 | def main(): |
1574 | | - """Main Streamlit application with Phase 1 improvements.""" |
| 1758 | + """Main Streamlit application with Phase 1, 2, and 3 improvements.""" |
1575 | 1759 | set_page_config() |
1576 | 1760 |
|
1577 | 1761 | st.title("🍽️ Tackle Hunger Data Explorer") |
@@ -1637,7 +1821,8 @@ def main(): |
1637 | 1821 | "Quality Analytics", |
1638 | 1822 | "Network Graph", |
1639 | 1823 | "📥 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 |
1641 | 1826 | ] |
1642 | 1827 | ) |
1643 | 1828 |
|
@@ -1678,6 +1863,9 @@ def main(): |
1678 | 1863 | elif page == "✅ Validation Recommendations": |
1679 | 1864 | # Phase 2: Validation recommendations page |
1680 | 1865 | display_validation_recommendations(sites, organizations) |
| 1866 | + elif page == "🗺️ Interactive Map": |
| 1867 | + # Phase 3.1: Interactive map |
| 1868 | + display_interactive_map(sites) |
1681 | 1869 |
|
1682 | 1870 | # Footer |
1683 | 1871 | st.markdown("---") |
|
0 commit comments