mirror of
https://github.com/alex-shpak/hugo-book.git
synced 2025-07-16 19:51:22 +00:00
map creation
created map
This commit is contained in:
parent
7a58953fbf
commit
c1be84474c
485
layouts/shortcodes/ai-bills-map copy.html
Normal file
485
layouts/shortcodes/ai-bills-map copy.html
Normal file
@ -0,0 +1,485 @@
|
||||
{{/*
|
||||
Usage: {{< ai-bills-map sheet-url="YOUR_GOOGLE_SHEET_CSV_URL" >}}
|
||||
*/}}
|
||||
|
||||
{{ $sheetUrl := .Get "sheet-url" }}
|
||||
|
||||
<div id="ai-bills-container" style="height: 600px; position: relative; border: 1px solid #ddd; border-radius: 8px; overflow: hidden;">
|
||||
<!-- Map Container -->
|
||||
<div id="ai-bills-map" style="height: 100%; width: 70%; float: left;"></div>
|
||||
|
||||
<!-- Sidebar Container -->
|
||||
<div id="ai-bills-sidebar" style="height: 100%; width: 30%; float: right; background: #f8f9fa; border-left: 1px solid #ddd; overflow-y: auto; padding: 15px; box-sizing: border-box;">
|
||||
<div id="ai-bills-content">
|
||||
<h3 style="margin-top: 0; color: #333; font-size: 1.2em;">AI Legislation Bills</h3>
|
||||
<p style="color: #666; margin-bottom: 20px;">Click on a state to view AI-related bills.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""/>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
|
||||
<!-- Papa Parse for CSV processing -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
|
||||
|
||||
<style>
|
||||
#ai-bills-container {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.state-hover {
|
||||
fill-opacity: 0.7 !important;
|
||||
stroke: #2c3e50 !important;
|
||||
stroke-width: 2 !important;
|
||||
}
|
||||
|
||||
.state-selected {
|
||||
fill: #3498db !important;
|
||||
fill-opacity: 0.8 !important;
|
||||
stroke: #2c3e50 !important;
|
||||
stroke-width: 3 !important;
|
||||
}
|
||||
|
||||
#ai-bills-sidebar h3 {
|
||||
color: #2c3e50;
|
||||
border-bottom: 2px solid #3498db;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.bill-item {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.bill-item:hover {
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.bill-number {
|
||||
font-weight: bold;
|
||||
color: #3498db;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.bill-date {
|
||||
color: #7f8c8d;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.bill-action {
|
||||
color: #2c3e50;
|
||||
margin: 5px 0;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.bill-description {
|
||||
color: #555;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.bill-link {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bill-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.state-info {
|
||||
background: #ecf0f1;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #7f8c8d;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#ai-bills-container {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
#ai-bills-map, #ai-bills-sidebar {
|
||||
width: 100% !important;
|
||||
float: none !important;
|
||||
}
|
||||
|
||||
#ai-bills-map {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
#ai-bills-sidebar {
|
||||
border-left: none !important;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const SHEET_URL = "{{ $sheetUrl }}";
|
||||
let map, billsData = {}, geojsonLayer;
|
||||
|
||||
// State name to abbreviation mapping
|
||||
const stateAbbreviations = {
|
||||
'Alabama': 'AL', 'Alaska': 'AK', 'Arizona': 'AZ', 'Arkansas': 'AR', 'California': 'CA',
|
||||
'Colorado': 'CO', 'Connecticut': 'CT', 'Delaware': 'DE', 'Florida': 'FL', 'Georgia': 'GA',
|
||||
'Hawaii': 'HI', 'Idaho': 'ID', 'Illinois': 'IL', 'Indiana': 'IN', 'Iowa': 'IA',
|
||||
'Kansas': 'KS', 'Kentucky': 'KY', 'Louisiana': 'LA', 'Maine': 'ME', 'Maryland': 'MD',
|
||||
'Massachusetts': 'MA', 'Michigan': 'MI', 'Minnesota': 'MN', 'Mississippi': 'MS', 'Missouri': 'MO',
|
||||
'Montana': 'MT', 'Nebraska': 'NE', 'Nevada': 'NV', 'New Hampshire': 'NH', 'New Jersey': 'NJ',
|
||||
'New Mexico': 'NM', 'New York': 'NY', 'North Carolina': 'NC', 'North Dakota': 'ND', 'Ohio': 'OH',
|
||||
'Oklahoma': 'OK', 'Oregon': 'OR', 'Pennsylvania': 'PA', 'Rhode Island': 'RI', 'South Carolina': 'SC',
|
||||
'South Dakota': 'SD', 'Tennessee': 'TN', 'Texas': 'TX', 'Utah': 'UT', 'Vermont': 'VT',
|
||||
'Virginia': 'VA', 'Washington': 'WA', 'West Virginia': 'WV', 'Wisconsin': 'WI', 'Wyoming': 'WY'
|
||||
};
|
||||
|
||||
// Initialize the map
|
||||
function initMap() {
|
||||
map = L.map('ai-bills-map', {
|
||||
center: [39.8283, -98.5795], // Center of US
|
||||
zoom: 4,
|
||||
zoomControl: true,
|
||||
scrollWheelZoom: true,
|
||||
// Restrict map bounds to show US including Alaska and Hawaii
|
||||
maxBounds: [
|
||||
[15, -180], // Southwest coordinates (includes Hawaii)
|
||||
[72, -60] // Northeast coordinates (includes Alaska)
|
||||
],
|
||||
maxBoundsViscosity: 1.0
|
||||
});
|
||||
|
||||
// Add tile layer
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxZoom: 18,
|
||||
minZoom: 3
|
||||
}).addTo(map);
|
||||
|
||||
loadUSStates();
|
||||
loadBillsData();
|
||||
}
|
||||
|
||||
// Load US states GeoJSON
|
||||
function loadUSStates() {
|
||||
console.log('Loading US states GeoJSON...');
|
||||
|
||||
// Use a reliable US states GeoJSON source
|
||||
fetch('https://raw.githubusercontent.com/PublicaMundi/MappingAPI/master/data/geojson/us-states.json')
|
||||
.then(response => {
|
||||
console.log('GeoJSON response status:', response.status);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('US states GeoJSON loaded successfully');
|
||||
console.log('Number of states in GeoJSON:', data.features.length);
|
||||
console.log('Sample state names:', data.features.slice(0, 3).map(f => f.properties.NAME));
|
||||
addStatesToMap(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading US states from primary source:', error);
|
||||
// Fallback: try alternative GeoJSON source
|
||||
loadUSStatesAlternative();
|
||||
});
|
||||
}
|
||||
|
||||
// Alternative GeoJSON source for US states
|
||||
function loadUSStatesAlternative() {
|
||||
console.log('Trying alternative GeoJSON source...');
|
||||
|
||||
fetch('https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Alternative GeoJSON loaded, filtering for US states...');
|
||||
// Filter for US states only
|
||||
const usStates = {
|
||||
type: 'FeatureCollection',
|
||||
features: data.features.filter(feature =>
|
||||
feature.properties.ISO_A2 === 'US' &&
|
||||
stateAbbreviations[feature.properties.NAME_EN]
|
||||
)
|
||||
};
|
||||
console.log('Filtered US states count:', usStates.features.length);
|
||||
addStatesToMap(usStates);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading alternative US states data:', error);
|
||||
// Last resort: create a simple fallback message
|
||||
document.getElementById('ai-bills-content').innerHTML +=
|
||||
'<div style="color: red; margin-top: 10px;">Error loading map data. State selection may not work.</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// Add states to map with interaction
|
||||
function addStatesToMap(geojsonData) {
|
||||
console.log('Adding states to map...');
|
||||
|
||||
geojsonLayer = L.geoJSON(geojsonData, {
|
||||
style: function(feature) {
|
||||
// Handle different property names in different GeoJSON sources
|
||||
const stateName = feature.properties.NAME || feature.properties.NAME_EN || feature.properties.name;
|
||||
const stateAbbr = stateAbbreviations[stateName];
|
||||
const billCount = billsData[stateAbbr] ? billsData[stateAbbr].length : 0;
|
||||
|
||||
console.log(`State: ${stateName} (${stateAbbr}) - Bills: ${billCount}`);
|
||||
|
||||
return {
|
||||
fillColor: getBillsColor(billCount),
|
||||
weight: 1,
|
||||
opacity: 1,
|
||||
color: '#666',
|
||||
fillOpacity: 0.5
|
||||
};
|
||||
},
|
||||
onEachFeature: function(feature, layer) {
|
||||
// Handle different property names in different GeoJSON sources
|
||||
const stateName = feature.properties.NAME || feature.properties.NAME_EN || feature.properties.name;
|
||||
const stateAbbr = stateAbbreviations[stateName];
|
||||
|
||||
console.log(`Setting up interactions for: ${stateName} (${stateAbbr})`);
|
||||
|
||||
// Store state info for later popup updates
|
||||
layer.stateName = stateName;
|
||||
layer.stateAbbr = stateAbbr;
|
||||
|
||||
layer.on({
|
||||
mouseover: function(e) {
|
||||
const layer = e.target;
|
||||
layer.setStyle({
|
||||
weight: 3,
|
||||
color: '#2c3e50',
|
||||
fillOpacity: 0.7
|
||||
});
|
||||
|
||||
if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
|
||||
layer.bringToFront();
|
||||
}
|
||||
},
|
||||
mouseout: function(e) {
|
||||
geojsonLayer.resetStyle(e.target);
|
||||
},
|
||||
click: function(e) {
|
||||
console.log(`Clicked on state: ${stateName} (${stateAbbr})`);
|
||||
showStateBills(stateAbbr, stateName);
|
||||
highlightState(e.target);
|
||||
}
|
||||
});
|
||||
|
||||
// Initial popup setup - will be updated when data loads
|
||||
updateStatePopup(layer);
|
||||
}
|
||||
}).addTo(map);
|
||||
|
||||
// Fit map bounds to show all US states including Alaska and Hawaii
|
||||
const bounds = geojsonLayer.getBounds();
|
||||
map.fitBounds(bounds, {
|
||||
padding: [10, 10],
|
||||
maxZoom: 5
|
||||
});
|
||||
|
||||
console.log('States added to map successfully');
|
||||
}
|
||||
|
||||
// Update popup for a state layer
|
||||
function updateStatePopup(layer) {
|
||||
const billCount = billsData[layer.stateAbbr] ? billsData[layer.stateAbbr].length : 0;
|
||||
layer.bindPopup(`<strong>${layer.stateName}</strong><br>${billCount} AI-related bills`);
|
||||
}
|
||||
|
||||
// Get color based on number of bills
|
||||
function getBillsColor(count) {
|
||||
return count > 40 ? '#800026' :
|
||||
count > 30 ? '#BD0026' :
|
||||
count > 20 ? '#E31A1C' :
|
||||
count > 15 ? '#FC4E2A' :
|
||||
count > 10 ? '#FD8D3C' :
|
||||
count > 5 ? '#FEB24C' :
|
||||
count > 0 ? '#FED976' :
|
||||
'#FFEDA0';
|
||||
}
|
||||
|
||||
// Highlight selected state
|
||||
function highlightState(layer) {
|
||||
// Reset all states
|
||||
geojsonLayer.eachLayer(function(l) {
|
||||
geojsonLayer.resetStyle(l);
|
||||
});
|
||||
|
||||
// Highlight selected state
|
||||
layer.setStyle({
|
||||
weight: 3,
|
||||
color: '#2c3e50',
|
||||
fillColor: '#3498db',
|
||||
fillOpacity: 0.8
|
||||
});
|
||||
}
|
||||
|
||||
// Load bills data from Google Sheets
|
||||
function loadBillsData() {
|
||||
if (!SHEET_URL) {
|
||||
document.getElementById('ai-bills-content').innerHTML =
|
||||
'<div class="loading">Please provide a sheet-url parameter</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Loading data from:', SHEET_URL);
|
||||
document.getElementById('ai-bills-content').innerHTML =
|
||||
'<div class="loading">Loading bills data...</div>';
|
||||
|
||||
Papa.parse(SHEET_URL, {
|
||||
download: true,
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
dynamicTyping: true,
|
||||
complete: function(results) {
|
||||
console.log('CSV loaded successfully');
|
||||
console.log('Total rows:', results.data.length);
|
||||
console.log('Headers:', results.meta.fields);
|
||||
console.log('First few rows:', results.data.slice(0, 3));
|
||||
|
||||
if (results.errors && results.errors.length > 0) {
|
||||
console.warn('CSV parsing errors:', results.errors);
|
||||
}
|
||||
|
||||
processBillsData(results.data);
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error loading CSV:', error);
|
||||
document.getElementById('ai-bills-content').innerHTML =
|
||||
'<div class="loading">Error loading bills data: ' + error.message + '</div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Process and group bills data by state
|
||||
function processBillsData(data) {
|
||||
console.log('Processing bills data...');
|
||||
billsData = {};
|
||||
|
||||
data.forEach(function(row, index) {
|
||||
// Clean whitespace from keys
|
||||
const cleanRow = {};
|
||||
Object.keys(row).forEach(key => {
|
||||
cleanRow[key.trim()] = row[key];
|
||||
});
|
||||
|
||||
const state = cleanRow.state ? cleanRow.state.trim() : '';
|
||||
console.log(`Row ${index}: state = "${state}"`);
|
||||
|
||||
if (state) {
|
||||
if (!billsData[state]) {
|
||||
billsData[state] = [];
|
||||
}
|
||||
billsData[state].push({
|
||||
bill_number: cleanRow.bill_number || '',
|
||||
url: cleanRow.url || '',
|
||||
last_action_date: cleanRow.last_action_date || '',
|
||||
last_action: cleanRow.last_action || '',
|
||||
description: cleanRow.description || ''
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Processed bills data:', billsData);
|
||||
console.log('States found:', Object.keys(billsData));
|
||||
|
||||
// Update sidebar with summary
|
||||
const totalBills = Object.values(billsData).reduce((sum, bills) => sum + bills.length, 0);
|
||||
const stateCount = Object.keys(billsData).length;
|
||||
|
||||
document.getElementById('ai-bills-content').innerHTML = `
|
||||
<h3>AI Legislation Bills</h3>
|
||||
<div class="state-info">
|
||||
<p><strong>${totalBills}</strong> bills across <strong>${stateCount}</strong> states</p>
|
||||
<p>Click on a state to view AI-related bills.</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Update map colors and popups if states are already loaded
|
||||
if (geojsonLayer) {
|
||||
console.log('Updating map colors and popups...');
|
||||
geojsonLayer.eachLayer(function(layer) {
|
||||
const feature = layer.feature;
|
||||
const stateName = feature.properties.NAME || feature.properties.NAME_EN || feature.properties.name;
|
||||
const stateAbbr = stateAbbreviations[stateName];
|
||||
const billCount = billsData[stateAbbr] ? billsData[stateAbbr].length : 0;
|
||||
|
||||
// Update layer style
|
||||
layer.setStyle({
|
||||
fillColor: getBillsColor(billCount)
|
||||
});
|
||||
|
||||
// Update popup with correct bill count
|
||||
layer.bindPopup(`<strong>${stateName}</strong><br>${billCount} AI-related bills`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Show bills for selected state
|
||||
function showStateBills(stateAbbr, stateName) {
|
||||
const bills = billsData[stateAbbr] || [];
|
||||
const sidebar = document.getElementById('ai-bills-content');
|
||||
|
||||
if (bills.length === 0) {
|
||||
sidebar.innerHTML = `
|
||||
<h3>${stateName} AI Bills</h3>
|
||||
<div class="state-info">
|
||||
<p>No AI-related bills found for ${stateName}.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = `
|
||||
<h3>${stateName} AI Bills</h3>
|
||||
<div class="state-info">
|
||||
<strong>${bills.length}</strong> AI-related bills found
|
||||
</div>
|
||||
`;
|
||||
|
||||
bills.forEach(function(bill) {
|
||||
html += `
|
||||
<div class="bill-item">
|
||||
<div class="bill-number">${bill.bill_number}</div>
|
||||
${bill.url ? `<a href="${bill.url}" target="_blank" class="bill-link">View Bill</a>` : ''}
|
||||
${bill.last_action_date ? `<div class="bill-date">Last Action: ${bill.last_action_date}</div>` : ''}
|
||||
${bill.last_action ? `<div class="bill-action">${bill.last_action}</div>` : ''}
|
||||
${bill.description ? `<div class="bill-description">${bill.description}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
sidebar.innerHTML = html;
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initMap);
|
||||
} else {
|
||||
initMap();
|
||||
}
|
||||
})();
|
||||
</script>
|
515
layouts/shortcodes/ai-bills-map.html
Normal file
515
layouts/shortcodes/ai-bills-map.html
Normal file
@ -0,0 +1,515 @@
|
||||
{{/*
|
||||
Usage: {{< ai-bills-map sheet-url="YOUR_GOOGLE_SHEET_CSV_URL" >}}
|
||||
*/}}
|
||||
|
||||
{{ $sheetUrl := .Get "sheet-url" }}
|
||||
|
||||
<div id="ai-bills-container" style="height: 600px; position: relative; border: 1px solid #ddd; border-radius: 8px; overflow: hidden;">
|
||||
<!-- Map Container -->
|
||||
<div id="ai-bills-map" style="height: 100%; width: 70%; float: left;"></div>
|
||||
|
||||
<!-- Sidebar Container -->
|
||||
<div id="ai-bills-sidebar" style="height: 100%; width: 30%; float: right; background: #f8f9fa; border-left: 1px solid #ddd; overflow-y: auto; padding: 15px; box-sizing: border-box;">
|
||||
<div id="ai-bills-content">
|
||||
<h3 style="margin-top: 0; color: #333; font-size: 1.2em;">AI Legislation Bills</h3>
|
||||
<p style="color: #666; margin-bottom: 20px;">Click on a state to view AI-related bills.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leaflet CSS -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""/>
|
||||
|
||||
<!-- Leaflet JS -->
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
|
||||
<!-- Papa Parse for CSV processing -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.4.1/papaparse.min.js"></script>
|
||||
|
||||
<style>
|
||||
#ai-bills-container {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.state-hover {
|
||||
fill-opacity: 0.7 !important;
|
||||
stroke: #2c3e50 !important;
|
||||
stroke-width: 2 !important;
|
||||
}
|
||||
|
||||
.state-selected {
|
||||
fill: #3498db !important;
|
||||
fill-opacity: 0.8 !important;
|
||||
stroke: #2c3e50 !important;
|
||||
stroke-width: 3 !important;
|
||||
}
|
||||
|
||||
#ai-bills-sidebar h3 {
|
||||
color: #2c3e50;
|
||||
border-bottom: 2px solid #3498db;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.bill-item {
|
||||
background: white;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 10px;
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.bill-item:hover {
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.bill-number {
|
||||
font-weight: bold;
|
||||
color: #3498db;
|
||||
font-size: 1.1em;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.bill-date {
|
||||
color: #7f8c8d;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
background: #f8f9fa;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
display: inline-block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.bill-action {
|
||||
color: #2c3e50;
|
||||
margin: 5px 0;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.bill-description {
|
||||
color: #555;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.4;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.bill-link {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bill-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.state-info {
|
||||
background: #ecf0f1;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #7f8c8d;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#ai-bills-container {
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
#ai-bills-map, #ai-bills-sidebar {
|
||||
width: 100% !important;
|
||||
float: none !important;
|
||||
}
|
||||
|
||||
#ai-bills-map {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
#ai-bills-sidebar {
|
||||
border-left: none !important;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const SHEET_URL = "{{ $sheetUrl }}";
|
||||
let map, billsData = {}, geojsonLayer;
|
||||
|
||||
// State name to abbreviation mapping
|
||||
const stateAbbreviations = {
|
||||
'Alabama': 'AL', 'Alaska': 'AK', 'Arizona': 'AZ', 'Arkansas': 'AR', 'California': 'CA',
|
||||
'Colorado': 'CO', 'Connecticut': 'CT', 'Delaware': 'DE', 'Florida': 'FL', 'Georgia': 'GA',
|
||||
'Hawaii': 'HI', 'Idaho': 'ID', 'Illinois': 'IL', 'Indiana': 'IN', 'Iowa': 'IA',
|
||||
'Kansas': 'KS', 'Kentucky': 'KY', 'Louisiana': 'LA', 'Maine': 'ME', 'Maryland': 'MD',
|
||||
'Massachusetts': 'MA', 'Michigan': 'MI', 'Minnesota': 'MN', 'Mississippi': 'MS', 'Missouri': 'MO',
|
||||
'Montana': 'MT', 'Nebraska': 'NE', 'Nevada': 'NV', 'New Hampshire': 'NH', 'New Jersey': 'NJ',
|
||||
'New Mexico': 'NM', 'New York': 'NY', 'North Carolina': 'NC', 'North Dakota': 'ND', 'Ohio': 'OH',
|
||||
'Oklahoma': 'OK', 'Oregon': 'OR', 'Pennsylvania': 'PA', 'Rhode Island': 'RI', 'South Carolina': 'SC',
|
||||
'South Dakota': 'SD', 'Tennessee': 'TN', 'Texas': 'TX', 'Utah': 'UT', 'Vermont': 'VT',
|
||||
'Virginia': 'VA', 'Washington': 'WA', 'West Virginia': 'WV', 'Wisconsin': 'WI', 'Wyoming': 'WY'
|
||||
};
|
||||
|
||||
// Initialize the map
|
||||
function initMap() {
|
||||
map = L.map('ai-bills-map', {
|
||||
center: [39.8283, -98.5795], // Center of US
|
||||
zoom: 4,
|
||||
zoomControl: true,
|
||||
scrollWheelZoom: true,
|
||||
// Restrict map bounds to show US including Alaska and Hawaii
|
||||
maxBounds: [
|
||||
[15, -180], // Southwest coordinates (includes Hawaii)
|
||||
[72, -60] // Northeast coordinates (includes Alaska)
|
||||
],
|
||||
maxBoundsViscosity: 1.0
|
||||
});
|
||||
|
||||
// Add tile layer
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxZoom: 18,
|
||||
minZoom: 3
|
||||
}).addTo(map);
|
||||
|
||||
loadUSStates();
|
||||
loadBillsData();
|
||||
}
|
||||
|
||||
// Load US states GeoJSON
|
||||
function loadUSStates() {
|
||||
console.log('Loading US states GeoJSON...');
|
||||
|
||||
// Use a reliable US states GeoJSON source
|
||||
fetch('https://raw.githubusercontent.com/PublicaMundi/MappingAPI/master/data/geojson/us-states.json')
|
||||
.then(response => {
|
||||
console.log('GeoJSON response status:', response.status);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('US states GeoJSON loaded successfully');
|
||||
console.log('Number of states in GeoJSON:', data.features.length);
|
||||
console.log('Sample state names:', data.features.slice(0, 3).map(f => f.properties.NAME));
|
||||
addStatesToMap(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading US states from primary source:', error);
|
||||
// Fallback: try alternative GeoJSON source
|
||||
loadUSStatesAlternative();
|
||||
});
|
||||
}
|
||||
|
||||
// Alternative GeoJSON source for US states
|
||||
function loadUSStatesAlternative() {
|
||||
console.log('Trying alternative GeoJSON source...');
|
||||
|
||||
fetch('https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('Alternative GeoJSON loaded, filtering for US states...');
|
||||
// Filter for US states only
|
||||
const usStates = {
|
||||
type: 'FeatureCollection',
|
||||
features: data.features.filter(feature =>
|
||||
feature.properties.ISO_A2 === 'US' &&
|
||||
stateAbbreviations[feature.properties.NAME_EN]
|
||||
)
|
||||
};
|
||||
console.log('Filtered US states count:', usStates.features.length);
|
||||
addStatesToMap(usStates);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading alternative US states data:', error);
|
||||
// Last resort: create a simple fallback message
|
||||
document.getElementById('ai-bills-content').innerHTML +=
|
||||
'<div style="color: red; margin-top: 10px;">Error loading map data. State selection may not work.</div>';
|
||||
});
|
||||
}
|
||||
|
||||
// Add states to map with interaction
|
||||
function addStatesToMap(geojsonData) {
|
||||
console.log('Adding states to map...');
|
||||
|
||||
geojsonLayer = L.geoJSON(geojsonData, {
|
||||
style: function(feature) {
|
||||
// Handle different property names in different GeoJSON sources
|
||||
const stateName = feature.properties.NAME || feature.properties.NAME_EN || feature.properties.name;
|
||||
const stateAbbr = stateAbbreviations[stateName];
|
||||
const billCount = billsData[stateAbbr] ? billsData[stateAbbr].length : 0;
|
||||
|
||||
console.log(`State: ${stateName} (${stateAbbr}) - Bills: ${billCount}`);
|
||||
|
||||
return {
|
||||
fillColor: getBillsColor(billCount),
|
||||
weight: 1,
|
||||
opacity: 1,
|
||||
color: '#666',
|
||||
fillOpacity: 0.5
|
||||
};
|
||||
},
|
||||
onEachFeature: function(feature, layer) {
|
||||
// Handle different property names in different GeoJSON sources
|
||||
const stateName = feature.properties.NAME || feature.properties.NAME_EN || feature.properties.name;
|
||||
const stateAbbr = stateAbbreviations[stateName];
|
||||
|
||||
console.log(`Setting up interactions for: ${stateName} (${stateAbbr})`);
|
||||
|
||||
// Store state info for later popup updates
|
||||
layer.stateName = stateName;
|
||||
layer.stateAbbr = stateAbbr;
|
||||
|
||||
layer.on({
|
||||
mouseover: function(e) {
|
||||
const layer = e.target;
|
||||
layer.setStyle({
|
||||
weight: 3,
|
||||
color: '#2c3e50',
|
||||
fillOpacity: 0.7
|
||||
});
|
||||
|
||||
if (!L.Browser.ie && !L.Browser.opera && !L.Browser.edge) {
|
||||
layer.bringToFront();
|
||||
}
|
||||
},
|
||||
mouseout: function(e) {
|
||||
geojsonLayer.resetStyle(e.target);
|
||||
},
|
||||
click: function(e) {
|
||||
console.log(`Clicked on state: ${stateName} (${stateAbbr})`);
|
||||
showStateBills(stateAbbr, stateName);
|
||||
highlightState(e.target);
|
||||
}
|
||||
});
|
||||
|
||||
// Initial popup setup - will be updated when data loads
|
||||
updateStatePopup(layer);
|
||||
}
|
||||
}).addTo(map);
|
||||
|
||||
// Fit map bounds to show all US states including Alaska and Hawaii
|
||||
const bounds = geojsonLayer.getBounds();
|
||||
map.fitBounds(bounds, {
|
||||
padding: [10, 10],
|
||||
maxZoom: 5
|
||||
});
|
||||
|
||||
console.log('States added to map successfully');
|
||||
}
|
||||
|
||||
// Update popup for a state layer
|
||||
function updateStatePopup(layer) {
|
||||
const billCount = billsData[layer.stateAbbr] ? billsData[layer.stateAbbr].length : 0;
|
||||
layer.bindPopup(`<strong>${layer.stateName}</strong><br>${billCount} AI-related bills`);
|
||||
}
|
||||
|
||||
// Get color based on number of bills
|
||||
function getBillsColor(count) {
|
||||
return count > 40 ? '#800026' :
|
||||
count > 30 ? '#BD0026' :
|
||||
count > 20 ? '#E31A1C' :
|
||||
count > 15 ? '#FC4E2A' :
|
||||
count > 10 ? '#FD8D3C' :
|
||||
count > 5 ? '#FEB24C' :
|
||||
count > 0 ? '#FED976' :
|
||||
'#FFEDA0';
|
||||
}
|
||||
|
||||
// Highlight selected state
|
||||
function highlightState(layer) {
|
||||
// Reset all states
|
||||
geojsonLayer.eachLayer(function(l) {
|
||||
geojsonLayer.resetStyle(l);
|
||||
});
|
||||
|
||||
// Highlight selected state
|
||||
layer.setStyle({
|
||||
weight: 3,
|
||||
color: '#2c3e50',
|
||||
fillColor: '#3498db',
|
||||
fillOpacity: 0.8
|
||||
});
|
||||
}
|
||||
|
||||
// Load bills data from Google Sheets
|
||||
function loadBillsData() {
|
||||
if (!SHEET_URL) {
|
||||
document.getElementById('ai-bills-content').innerHTML =
|
||||
'<div class="loading">Please provide a sheet-url parameter</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Loading data from:', SHEET_URL);
|
||||
document.getElementById('ai-bills-content').innerHTML =
|
||||
'<div class="loading">Loading bills data...</div>';
|
||||
|
||||
Papa.parse(SHEET_URL, {
|
||||
download: true,
|
||||
header: true,
|
||||
skipEmptyLines: true,
|
||||
dynamicTyping: true,
|
||||
complete: function(results) {
|
||||
console.log('CSV loaded successfully');
|
||||
console.log('Total rows:', results.data.length);
|
||||
console.log('Headers:', results.meta.fields);
|
||||
console.log('First few rows:', results.data.slice(0, 3));
|
||||
|
||||
if (results.errors && results.errors.length > 0) {
|
||||
console.warn('CSV parsing errors:', results.errors);
|
||||
}
|
||||
|
||||
processBillsData(results.data);
|
||||
},
|
||||
error: function(error) {
|
||||
console.error('Error loading CSV:', error);
|
||||
document.getElementById('ai-bills-content').innerHTML =
|
||||
'<div class="loading">Error loading bills data: ' + error.message + '</div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Process and group bills data by state
|
||||
function processBillsData(data) {
|
||||
console.log('Processing bills data...');
|
||||
billsData = {};
|
||||
|
||||
data.forEach(function(row, index) {
|
||||
// Clean whitespace from keys
|
||||
const cleanRow = {};
|
||||
Object.keys(row).forEach(key => {
|
||||
cleanRow[key.trim()] = row[key];
|
||||
});
|
||||
|
||||
const state = cleanRow.state ? cleanRow.state.trim() : '';
|
||||
console.log(`Row ${index}: state = "${state}"`);
|
||||
|
||||
if (state) {
|
||||
if (!billsData[state]) {
|
||||
billsData[state] = [];
|
||||
}
|
||||
billsData[state].push({
|
||||
bill_number: cleanRow.bill_number || '',
|
||||
url: cleanRow.url || '',
|
||||
last_action_date: cleanRow.last_action_date || '',
|
||||
last_action: cleanRow.last_action || '',
|
||||
description: cleanRow.description || ''
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Processed bills data:', billsData);
|
||||
console.log('States found:', Object.keys(billsData));
|
||||
|
||||
// Update sidebar with summary
|
||||
const totalBills = Object.values(billsData).reduce((sum, bills) => sum + bills.length, 0);
|
||||
const stateCount = Object.keys(billsData).length;
|
||||
|
||||
document.getElementById('ai-bills-content').innerHTML = `
|
||||
<h3>AI Legislation Bills</h3>
|
||||
<div class="state-info">
|
||||
<p><strong>${totalBills}</strong> bills across <strong>${stateCount}</strong> states</p>
|
||||
<p>Click on a state to view AI-related bills.</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Update map colors and popups if states are already loaded
|
||||
if (geojsonLayer) {
|
||||
console.log('Updating map colors and popups...');
|
||||
geojsonLayer.eachLayer(function(layer) {
|
||||
const feature = layer.feature;
|
||||
const stateName = feature.properties.NAME || feature.properties.NAME_EN || feature.properties.name;
|
||||
const stateAbbr = stateAbbreviations[stateName];
|
||||
const billCount = billsData[stateAbbr] ? billsData[stateAbbr].length : 0;
|
||||
|
||||
// Update layer style
|
||||
layer.setStyle({
|
||||
fillColor: getBillsColor(billCount)
|
||||
});
|
||||
|
||||
// Update popup with correct bill count
|
||||
layer.bindPopup(`<strong>${stateName}</strong><br>${billCount} AI-related bills`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Show bills for selected state
|
||||
function showStateBills(stateAbbr, stateName) {
|
||||
const bills = billsData[stateAbbr] || [];
|
||||
const sidebar = document.getElementById('ai-bills-content');
|
||||
|
||||
if (bills.length === 0) {
|
||||
sidebar.innerHTML = `
|
||||
<h3>${stateName} AI Bills</h3>
|
||||
<div class="state-info">
|
||||
<p>No AI-related bills found for ${stateName}.</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort bills by last action date (most recent first)
|
||||
const sortedBills = bills.slice().sort(function(a, b) {
|
||||
const dateA = a.last_action_date ? new Date(a.last_action_date) : new Date('1900-01-01');
|
||||
const dateB = b.last_action_date ? new Date(b.last_action_date) : new Date('1900-01-01');
|
||||
return dateB - dateA; // Descending order (newest first)
|
||||
});
|
||||
|
||||
let html = `
|
||||
<h3>${stateName} AI Bills</h3>
|
||||
<div class="state-info">
|
||||
<strong>${bills.length}</strong> AI-related bills found
|
||||
<div style="font-size: 0.85em; color: #666; margin-top: 5px;">Sorted by latest action date</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
sortedBills.forEach(function(bill) {
|
||||
// Format the date nicely
|
||||
let formattedDate = '';
|
||||
if (bill.last_action_date) {
|
||||
try {
|
||||
const date = new Date(bill.last_action_date);
|
||||
formattedDate = date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch (e) {
|
||||
formattedDate = bill.last_action_date;
|
||||
}
|
||||
}
|
||||
|
||||
html += `
|
||||
<div class="bill-item">
|
||||
<div class="bill-number">${bill.bill_number}</div>
|
||||
${formattedDate ? `<div class="bill-date">Last Action: ${formattedDate}</div>` : ''}
|
||||
${bill.url ? `<div class="bill-link-container"><a href="${bill.url}" target="_blank" class="bill-link">View Bill</a></div>` : ''}
|
||||
${bill.last_action ? `<div class="bill-action">${bill.last_action}</div>` : ''}
|
||||
${bill.description ? `<div class="bill-description">${bill.description}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
sidebar.innerHTML = html;
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initMap);
|
||||
} else {
|
||||
initMap();
|
||||
}
|
||||
})();
|
||||
</script>
|
7646
static/statemap/statemap_data.json
Normal file
7646
static/statemap/statemap_data.json
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user