// @ts-nocheck import { useState, useRef, useEffect } from 'react' import { Link } from 'react-router-dom' import { ComposableMap, Geographies, Geography } from 'react-simple-maps' const geoUrl = 'https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json' interface BillSample { bill_number: string title: string status: string type: string action: string date?: string // Format: "Jan 2026" state: string } interface StateData { state: string total_bills: number type_counts: { ban: number restriction: number protection: number other: number } status_counts: { enacted: number failed: number pending: number } primary_type: string primary_status: string map_category: string sample_bills?: BillSample[] } interface USMapProps { stateData: Record onStateClick?: (stateCode: string) => void legend?: { types: Record statuses: Record } } // State code to FIPS mapping const STATE_FIPS: Record = { 'AL': '01', 'AK': '02', 'AZ': '04', 'AR': '05', 'CA': '06', 'CO': '08', 'CT': '09', 'DE': '10', 'FL': '12', 'GA': '13', 'HI': '15', 'ID': '16', 'IL': '17', 'IN': '18', 'IA': '19', 'KS': '20', 'KY': '21', 'LA': '22', 'ME': '23', 'MD': '24', 'MA': '25', 'MI': '26', 'MN': '27', 'MS': '28', 'MO': '29', 'MT': '30', 'NE': '31', 'NV': '32', 'NH': '33', 'NJ': '34', 'NM': '35', 'NY': '36', 'NC': '37', 'ND': '38', 'OH': '39', 'OK': '40', 'OR': '41', 'PA': '42', 'RI': '44', 'SC': '45', 'SD': '46', 'TN': '47', 'TX': '48', 'UT': '49', 'VT': '50', 'VA': '51', 'WA': '53', 'WV': '54', 'WI': '55', 'WY': '56', 'DC': '11', 'PR': '72' } const FIPS_STATE: Record = Object.fromEntries( Object.entries(STATE_FIPS).map(([k, v]) => [v, k]) ) // Flexible color palette for different legislation types const TYPE_COLOR_PALETTE: Record = { // Fluoridation categories 'mandate': '#4CAF50', // Green - Mandate 'removal': '#F44336', // Red - Removal 'study': '#9C27B0', // Purple - Study // Dental/Oral Health categories 'coverage_expansion': '#4CAF50', // Green - Expansion 'screening': '#FF9800', // Orange - Screening 'provider_access': '#2196F3', // Blue - Provider Access // Medicaid categories 'expansion': '#4CAF50', // Green - Expansion 'coverage': '#2196F3', // Blue - Coverage 'reimbursement': '#FF9800', // Orange - Reimbursement 'eligibility': '#9C27B0', // Purple - Eligibility // Education categories 'requirement': '#FF9800', // Orange - Requirement 'curriculum': '#2196F3', // Blue - Curriculum 'reform': '#9C27B0', // Purple - Reform // Health/General categories 'protection': '#4CAF50', // Green - Protection 'restriction': '#F44336', // Red - Restriction // Generic categories 'support': '#4CAF50', // Green - Support 'oppose': '#F44336', // Red - Oppose 'regulate': '#FF9800', // Orange - Regulate // Shared 'funding': '#2196F3', // Blue - Funding (appears in multiple) 'other': '#9E9E9E', // Gray - Other } // Get color for any category with fallback const getColorForCategory = (category: string): string => { return TYPE_COLOR_PALETTE[category] || '#9E9E9E' // Default to gray } // Color scheme based on the user's description const getStateColor = (stateCode: string, stateData: Record): string => { const data = stateData[stateCode] if (!data || data.total_bills === 0) { return '#E3F2FD' // Light blue - no legislation } const { primary_type, primary_status } = data // Get base color for this type let baseColor = getColorForCategory(primary_type) // Adjust shade based on status if (primary_status === 'enacted') { // Slightly darker for enacted (reduce lightness) return adjustColorBrightness(baseColor, -20) } else if (primary_status === 'failed') { // Lighter for failed (increase lightness) return adjustColorBrightness(baseColor, 40) } return baseColor } // Helper to adjust color brightness const adjustColorBrightness = (hex: string, percent: number): string => { // Simple brightness adjustment const num = parseInt(hex.replace('#', ''), 16) const amt = Math.round(2.55 * percent) const R = (num >> 16) + amt const G = (num >> 8 & 0x00FF) + amt const B = (num & 0x0000FF) + amt return '#' + ( 0x1000000 + (R < 255 ? (R < 1 ? 0 : R) : 255) * 0x10000 + (G < 255 ? (G < 1 ? 0 : G) : 255) * 0x100 + (B < 255 ? (B < 1 ? 0 : B) : 255) ).toString(16).slice(1).toUpperCase() } const getPatternForState = (stateCode: string, stateData: Record): string | null => { const data = stateData[stateCode] if (!data || data.total_bills === 0) { return null } const { primary_status } = data if (primary_status === 'failed') { return 'crosshatch' } else if (primary_status === 'enacted') { return 'diagonal' } return null } export default function USMap({ stateData, onStateClick, legend }: USMapProps) { const [hoveredState, setHoveredState] = useState(null) const [tooltipPosition, setTooltipPosition] = useState({ x: 0, y: 0 }) const hoverTimeoutRef = useRef(null) const hoveredStateElementRef = useRef(null) const containerRef = useRef(null) // Get unique types from actual state data if legend not provided const legislationTypes = legend?.types || {} const legislationStatuses = legend?.statuses || { 'enacted': 'Enacted', 'failed': 'Failed', 'pending': 'Pending' } // Helper to calculate position relative to container const calculateRelativePosition = (element: any) => { if (!element || !containerRef.current) return { x: 0, y: 0 } const stateBounds = element.getBoundingClientRect() const containerBounds = containerRef.current.getBoundingClientRect() return { x: stateBounds.left - containerBounds.left + stateBounds.width / 2, y: stateBounds.top - containerBounds.top } } // Update tooltip position on scroll useEffect(() => { const updateTooltipPosition = () => { if (hoveredState && hoveredStateElementRef.current) { setTooltipPosition(calculateRelativePosition(hoveredStateElementRef.current)) } } if (hoveredState) { window.addEventListener('scroll', updateTooltipPosition, true) return () => window.removeEventListener('scroll', updateTooltipPosition, true) } }, [hoveredState]) const handleMouseEnter = (event: any, stateCode: string) => { // Clear any pending state change if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current) hoverTimeoutRef.current = null } // Store the element reference for scroll updates hoveredStateElementRef.current = event.target // If no tooltip showing, show immediately if (!hoveredState) { setHoveredState(stateCode) setTooltipPosition(calculateRelativePosition(event.target)) } // If tooltip already showing for different state, delay switch // This prevents accidental switches when moving mouse to tooltip else if (hoveredState !== stateCode) { hoverTimeoutRef.current = setTimeout(() => { setHoveredState(stateCode) setTooltipPosition(calculateRelativePosition(event.target)) }, 200) // 200ms delay prevents accidental switches } } const handleMouseLeave = () => { // Clear any pending state change when leaving if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current) hoverTimeoutRef.current = null } } // Close button handler const handleCloseTooltip = () => { setHoveredState(null) if (hoverTimeoutRef.current) { clearTimeout(hoverTimeoutRef.current) hoverTimeoutRef.current = null } } const hoveredData = hoveredState ? stateData[hoveredState] : null return (
{/* SVG Patterns for overlays */} {/* Crosshatch pattern for failed */} {/* Diagonal stripes for enacted */} {({ geographies }) => geographies.map((geo) => { const fips = geo.id const stateCode = FIPS_STATE[fips] || fips const data = stateData[stateCode] const pattern = getPatternForState(stateCode, stateData) return ( onStateClick?.(stateCode)} onMouseEnter={(event) => handleMouseEnter(event, stateCode)} onMouseLeave={handleMouseLeave} /> ) }) } {/* Tooltip - Stays visible until hover another state or click close */} {hoveredState && hoveredData && (
{/* Close button */}
{hoveredState}
{hoveredData.total_bills.toLocaleString()} bill{hoveredData.total_bills !== 1 ? 's' : ''}
{hoveredData.total_bills > 0 && ( <> {/* Primary Type and Status - Prominent */}
{legislationTypes[hoveredData.primary_type] || hoveredData.primary_type}
Primary Type
{hoveredData.primary_status === 'enacted' ? '✓ Enacted' : hoveredData.primary_status === 'failed' ? '✗ Failed' : '⏳ Pending'}
{/* Sample Bills Grouped by Type */} {hoveredData.sample_bills && hoveredData.sample_bills.length > 0 && (
Recent Bills by Type:
{(() => { // Group bills by type const billsByType = hoveredData.sample_bills.reduce((acc, bill) => { if (!acc[bill.type]) acc[bill.type] = [] acc[bill.type].push(bill) return acc }, {} as Record) return Object.entries(billsByType).map(([type, bills]) => (
{legislationTypes[type] || type} ({bills.length})
{bills.map((bill, idx) => (
{bill.bill_number} {bill.status === 'enacted' ? '✓ Enacted' : bill.status === 'failed' ? '✗ Failed' : '⏳ Pending'}
{bill.date && 📅 {bill.date}} {bill.date && bill.action && } {bill.action || 'Click for details'}
))}
)) })()}
)} {/* Status Summary */}
{hoveredData.status_counts.enacted}
Enacted
{hoveredData.status_counts.pending}
Pending
{hoveredData.status_counts.failed}
Failed
{/* Drill Down Button */} )} {hoveredData.total_bills === 0 && (
No legislation found
)} {/* Tooltip arrow */}
)} {/* Legend - Absolute on desktop, below map on mobile */}
Legend
{/* Type of Legislation - Show actual types from data */} {(() => { // Get unique types from actual state data const uniqueTypes = new Set() Object.values(stateData).forEach(state => { if (state.primary_type) uniqueTypes.add(state.primary_type) }) return uniqueTypes.size > 0 && (
Type of Legislation
{Array.from(uniqueTypes).sort().map(type => (
{legislationTypes[type] || type.replace(/_/g, ' ')}
))}
No Legislation
) })()} {/* Status of Legislation */}
Status
Enacted (darker)
Pending (normal)
Failed (lighter)
) }