Creating Legislative Tracking Maps
Learn how to download state legislation data and create choropleth maps showing legislative activity across multiple social issues.
šÆ What You'll Createā
Interactive maps similar to legislative tracking visualizations that show:
- Type of Legislation: Outright Ban, Restriction, Protection
- Status: Introduced (pending), Enacted (passed), Failed (defeated)
- Geographic Distribution: State-by-state view of legislative activity
Example use cases:
- Water fluoridation legislation tracker
- Abortion access legislation map
- Marijuana legalization progress
- Voting rights legislation
- LGBTQ+ protection laws
- Education policy (CRT, book bans, etc.)
š ļø Prerequisitesā
Choose Your Data Sourceā
ā” RECOMMENDED: Bulk Downloads (Faster & Easier)
Plural Policy offers bulk downloads of ALL state legislation - no API key needed!
# Download all 2024 legislation for all 50 states (CSV)
python scripts/bulk_legislative_download.py --year 2024 --format csv --merge
# Output: data/cache/legislation_bulk/all_states_2024.csv
# Contains: ALL bills from ALL states in one file!
Advantages:
- ā No API key required - Public bulk data
- ā No rate limits - Download encouraged
- ā Faster - One download vs thousands of API calls
- ā Complete - Entire legislative sessions
- ā Offline - Process locally without internet
Alternative: Open States API (Real-time)
For real-time bill tracking and search:
# Sign up: https://openstates.org/accounts/signup/
# Add to .env:
OPENSTATES_API_KEY=your-key-here
Use API when you need:
- Latest bill status (same-day updates)
- Search by specific keywords
- Subset of bills (not entire sessions)
1. Install Required Packagesā
# Core dependencies
pip install httpx pandas loguru python-dotenv
# Visualization libraries
pip install plotly matplotlib
2. Get Open States API Key (Free)ā
Open States provides state legislation data for all 50 states.
- Sign up: https://openstates.org/accounts/signup/
- Get API key: https://openstates.org/accounts/profile/
- Add to
.env:OPENSTATES_API_KEY=your-key-here
Free Tier:
- 50,000 requests per month
- Access to all 50 states
- Bill text, voting records, legislators
š Quick Startā
Method 1: Bulk Download (Recommended)ā
Step 1: Download all 2024 legislation
# Download CSV files for all 50 states
python scripts/bulk_legislative_download.py --year 2024 --format csv --merge
Step 2: Process and categorize
from scripts.legislative_tracker import LegislativeTracker
import pandas as pd
tracker = LegislativeTracker()
# Load bulk downloaded data
df = pd.read_csv('data/cache/legislation_bulk/all_states_2024.csv')
# Categorize bills for fluoridation tracking
categorized = []
for _, bill in df.iterrows():
cat = tracker.categorize_bill(bill.to_dict(), 'fluoridation')
categorized.append(cat)
df_categorized = pd.DataFrame(categorized)
# Generate map
tracker.create_choropleth_map(df_categorized, "fluoridation")
Output:
- Downloaded: All 50 states in minutes (not hours)
- Processed: Categorized by issue type
- Map:
data/visualizations/fluoridation_map.html
Method 2: Open States API (Real-time)ā
Step 1: Get API key
# Sign up at https://openstates.org/accounts/signup/
# Add to .env:
OPENSTATES_API_KEY=your-key-here
Step 2: Track issue
# Track fluoridation legislation in 2024
python scripts/legislative_tracker.py \
--issue fluoridation \
--year 2024 \
--visualize
Output:
data/cache/legislation/fluoridation_2024.csv- Raw bill datadata/visualizations/fluoridation_map.html- Interactive mapdata/visualizations/fluoridation_legend.png- Color legend
Track Multiple Issuesā
from scripts.legislative_tracker import LegislativeTracker
import asyncio
async def track_all_issues():
tracker = LegislativeTracker()
issues = ["fluoridation", "abortion", "marijuana", "voting", "lgbtq", "education"]
for issue in issues:
print(f"\nš Tracking {issue} legislation...")
df = await tracker.track_issue(issue, year=2024)
tracker.create_choropleth_map(df, issue)
print(f"ā
{issue}: {len(df)} bills tracked")
asyncio.run(track_all_issues())
š How It Worksā
1. Data Collection (Open States API)ā
The LegislativeTracker class searches Open States API for bills matching issue keywords:
tracker = LegislativeTracker()
# Search for fluoridation bills in all states
bills = await tracker.search_bills("fluoridation", year=2024)
# Returns bill metadata:
# - Title, summary, bill number
# - State, session, sponsors
# - Actions, votes, committee assignments
2. Bill Categorizationā
Each bill is categorized by type and status:
Bill Typesā
Determined by keyword matching in title/summary:
| Type | Keywords | Example |
|---|---|---|
| Ban | "prohibit", "ban", "criminalize" | "Prohibit Water Fluoridation Act" |
| Restriction | "limit", "restrict", "regulate", "parental consent" | "Fluoride Disclosure Requirements" |
| Protection | "require", "mandate", "protect", "expand" | "Community Water Fluoridation Mandate" |
Bill Statusā
Determined by latest legislative action:
| Status | Action Keywords | Meaning |
|---|---|---|
| Enacted | "signed", "enacted", "passed", "approved" | Bill became law |
| Introduced | "introduced", "referred", "committee" | Bill is pending |
| Failed | "failed", "defeated", "vetoed", "withdrawn" | Bill did not pass |
Categorization Logic:
def categorize_bill(bill, issue):
text = f"{bill['title']} {bill['summary']}".lower()
# Check for ban keywords
if any(kw in text for kw in ["prohibit fluoride", "ban fluoridation"]):
bill_type = "ban"
# Check for restriction keywords
elif any(kw in text for kw in ["restrict fluoride", "limit fluoridation"]):
bill_type = "restriction"
# Check for protection keywords
elif any(kw in text for kw in ["require fluoride", "mandate fluoridation"]):
bill_type = "protection"
return bill_type
3. State-Level Aggregationā
Bills are grouped by state to determine:
- Dominant legislation type (ban/restriction/protection)
- Dominant status (enacted/introduced/failed)
- Bill counts per category
# Example state summary
{
"state_code": "AL",
"dominant_type": "ban",
"dominant_status": "introduced",
"total_bills": 3,
"ban_count": 2,
"restriction_count": 1,
"protection_count": 0
}
4. Map Visualizationā
Color Coding:
| Color | Bill Type | Status | Example |
|---|---|---|---|
| š¤ Dark Brown | Ban | Enacted | Fluoridation ban passed into law |
| š Orange | Ban | Introduced | Fluoridation ban pending |
| š” Light Yellow | Ban | Failed | Fluoridation ban defeated |
| šØ Goldenrod | Restriction | Enacted | Disclosure law passed |
| š Gold | Restriction | Introduced | Restriction pending |
| š” Pale Yellow | Restriction | Failed | Restriction defeated |
| šµ Dark Blue | Protection | Enacted | Fluoridation mandate passed |
| š· Royal Blue | Protection | Introduced | Protection bill pending |
| āļø Sky Blue | Protection | Failed | Protection defeated |
Visual Patterns:
- Solid colors = bills passed into law
- Lighter shades = bills introduced but pending
- Palest shades = bills failed/defeated
š Example: Fluoridation Legislation Mapā
Step 1: Search for Billsā
import asyncio
from scripts.legislative_tracker import LegislativeTracker
async def main():
tracker = LegislativeTracker()
# Track fluoridation bills in 2024
df = await tracker.track_issue("fluoridation", year=2024)
# View results
print(df[['state_code', 'title', 'type', 'status']])
asyncio.run(main())
Output:
state_code title type status
AL Prohibit Water Fluoridation ban introduced
CA Community Fluoridation Mandate protection enacted
TX Fluoride Disclosure Requirements restriction introduced
Step 2: Generate Mapā
# Create interactive choropleth map
tracker.create_choropleth_map(df, "fluoridation")
Output: data/visualizations/fluoridation_map.html
Step 3: View Mapā
Open fluoridation_map.html in a browser to see:
- Color-coded states by legislation type/status
- Hover over states for bill details
- Interactive zoom and pan
- Legend showing color meanings
šØ Customizing Issue Keywordsā
Add your own issue keywords to track new topics:
tracker = LegislativeTracker()
# Add custom issue keywords
tracker.issue_keywords["gun_control"] = {
"ban": ["ban assault weapons", "prohibit firearms", "gun ban"],
"restriction": ["background check", "waiting period", "permit requirement"],
"protection": ["constitutional carry", "second amendment protection", "gun rights"]
}
# Track the new issue
df = await tracker.track_issue("gun_control", year=2024)
š Data Sourcesā
Plural Policy Bulk Downloads ā Recommendedā
What it provides:
- Complete legislative sessions - All bills, votes, sponsors
- CSV format - Easy to process with pandas
- JSON format - Includes full bill text
- PostgreSQL dumps - Entire database (monthly snapshots)
- No rate limits - Bulk downloads encouraged
- No API key - Public domain data
Coverage:
- ā All 50 states + DC, PR
- ā Historical sessions (2010+)
- ā Current sessions (monthly updates)
- ā Complete bill lifecycle
Bulk Download URLs:
- CSV per session: https://data.openstates.org/session/csv/{state}/{session_id}.csv
- JSON per session: https://data.openstates.org/session/json/{state}/{session_id}.json.zip
- PostgreSQL dump: https://data.openstates.org/postgres/monthly/YYYY-MM-public.pgdump
- Documentation: https://open.pluralpolicy.com/data/
Download Options:
# Option 1: All states as CSV (fast, ~500MB total)
python scripts/bulk_legislative_download.py --year 2024 --format csv --merge
# Option 2: Specific states as JSON (includes full text)
python scripts/bulk_legislative_download.py --year 2024 --states CA,TX,NY --format json
# Option 3: PostgreSQL dump (complete database, ~5GB)
python scripts/bulk_legislative_download.py --postgres --month 2026-04
Why use bulk downloads?
| Feature | Bulk Download | API |
|---|---|---|
| Speed | ā Minutes for all states | ā Hours (50K API calls) |
| Rate Limits | ā None | ā ļø 50K/month |
| API Key | ā Not required | ā Required |
| Offline | ā Process locally | ā Needs internet |
| Complete Sessions | ā All bills at once | ā ļø Must paginate |
| Real-time | ā ļø Monthly updates | ā Same-day updates |
Recommendation: Use bulk downloads for historical analysis and map generation. Use API for real-time bill tracking.
Open States API (Real-time Updates)ā
What it provides:
- 100,000+ state bills from all 50 states
- Bill text, summaries, sponsors
- Legislative actions and votes
- Committee assignments
- Real-time updates
Coverage:
- ā All 50 states + DC, PR, territories
- ā Current and historical sessions
- ā Multiple bill types (HB, SB, HR, SR, etc.)
- ā Standardized OCD-ID format
API Documentation: https://docs.openstates.org/api-v3/
Free Tier:
- 50,000 requests/month
- No credit card required
- Commercial and non-commercial use
Ballotpedia (Optional)ā
For ballot measures and referendums (not just legislation):
from discovery.ballotpedia_integration import BallotpediaDiscovery
discovery = BallotpediaDiscovery()
measures = await discovery.get_ballot_measures("Alabama", year=2024)
Note: Ballotpedia API is paid. Web scraping fallback available.
šļø Output Filesā
CSV Data Filesā
Location: data/cache/legislation/{issue}_{year}.csv
Columns:
bill_id- State bill number (e.g., "HB 123")state- State name (e.g., "Alabama")state_code- Two-letter code (e.g., "AL")title- Bill titletype- Categorization (ban/restriction/protection)status- Legislative status (introduced/enacted/failed)url- Open States bill page URLsession- Legislative sessionlatest_action- Most recent actionlatest_action_date- Date of action
Example:
bill_id,state,state_code,title,type,status,url
HB 123,Alabama,AL,Prohibit Water Fluoridation,ban,introduced,https://openstates.org/al/bills/2024/HB123/
SB 456,California,CA,Community Fluoridation Mandate,protection,enacted,https://openstates.org/ca/bills/2024/SB456/
HTML Map Filesā
Location: data/visualizations/{issue}_map.html
Features:
- Interactive choropleth map
- Hover tooltips with bill details
- Zoom/pan controls
- Responsive design
- Embeddable in websites
Legend Imagesā
Location: data/visualizations/{issue}_legend.png
Contains:
- Color key for bill types
- Status indicators
- Pattern explanations
š§ Advanced Usageā
Track Specific States Onlyā
# Track only southern states
southern_states = ["AL", "AR", "FL", "GA", "KY", "LA", "MS", "NC", "SC", "TN", "TX", "VA", "WV"]
df = await tracker.track_issue("fluoridation", year=2024, states=southern_states)
Multi-Year Trendsā
import pandas as pd
dfs = []
for year in range(2020, 2025):
df = await tracker.track_issue("fluoridation", year=year)
df['year'] = year
dfs.append(df)
# Combine all years
all_years = pd.concat(dfs, ignore_index=True)
# Analyze trends
trend = all_years.groupby(['year', 'type']).size().reset_index(name='count')
print(trend)
Output:
year type count
2020 ban 12
2020 restriction 8
2020 protection 5
2021 ban 15
2021 restriction 10
...
Export for Further Analysisā
# Export to Excel with multiple sheets
with pd.ExcelWriter('fluoridation_analysis.xlsx') as writer:
df.to_excel(writer, sheet_name='All Bills', index=False)
# Summary by state
state_summary = tracker.generate_state_summary(df)
state_summary.to_excel(writer, sheet_name='State Summary', index=False)
# Enacted bills only
enacted = df[df['status'] == 'enacted']
enacted.to_excel(writer, sheet_name='Enacted Bills', index=False)
šÆ Use Casesā
1. Advocacy Campaign Planningā
Goal: Identify states with active legislation for targeted campaigns
# Find states with pending ban legislation
df = await tracker.track_issue("fluoridation", year=2024)
pending_bans = df[(df['type'] == 'ban') & (df['status'] == 'introduced')]
print(f"States with pending fluoridation bans: {pending_bans['state_code'].unique()}")
# Output: ['AL', 'TX', 'FL', ...]
Use for:
- Email campaigns to state legislators
- Social media targeting by state
- Coalition building in key states
2. Policy Researchā
Goal: Track legislative trends over time
# Compare ban vs protection bills over 5 years
for year in range(2020, 2025):
df = await tracker.track_issue("fluoridation", year=year)
bans = len(df[df['type'] == 'ban'])
protections = len(df[df['type'] == 'protection'])
print(f"{year}: {bans} bans, {protections} protections")
3. Media and Journalismā
Goal: Create data-driven stories on legislative activity
# Generate map for publication
df = await tracker.track_issue("fluoridation", year=2024)
tracker.create_choropleth_map(df, "fluoridation", output_file="public/fluoride_map.html")
# Embed in article with <iframe>
4. Academic Researchā
Goal: Analyze correlation between legislation and demographics
# Merge with Census data
import geopandas as gpd
df = await tracker.track_issue("fluoridation", year=2024)
state_summary = tracker.generate_state_summary(df)
# Join with state-level Census data
census = pd.read_csv("census_state_demographics.csv")
merged = state_summary.merge(census, on='state_code')
# Analyze correlations
correlation = merged[['ban_count', 'median_income', 'college_educated_pct']].corr()
š Next Stepsā
Integrate with Knowledge Graphā
Add legislation to the jurisdiction knowledge graph:
from neo4j import GraphDatabase
driver = GraphDatabase.driver("bolt://localhost:7687")
with driver.session() as session:
for _, bill in df.iterrows():
session.run("""
MATCH (j:Jurisdiction {state_code: $state_code})
CREATE (b:Bill {
bill_id: $bill_id,
title: $title,
type: $type,
status: $status,
url: $url
})
CREATE (j)-[:HAS_LEGISLATION]->(b)
""", bill.to_dict())
Add to Dashboardā
Embed map in React frontend:
// frontend/src/pages/LegislationTracker.tsx
import React from 'react';
export function LegislationMap({ issue }: { issue: string }) {
return (
<iframe
src={`/data/visualizations/${issue}_map.html`}
width="100%"
height="600px"
frameBorder="0"
/>
);
}
Automate Daily Updatesā
Create cron job to update data:
# crontab -e
# Run daily at 6am
0 6 * * * cd /path/to/project && python scripts/legislative_tracker.py --issue fluoridation --year 2024 --visualize
š Related Documentationā
ā Troubleshootingā
Error: "OPENSTATES_API_KEY not found"ā
Solution: Add API key to .env:
OPENSTATES_API_KEY=your-key-here
Error: "Plotly not installed"ā
Solution: Install visualization libraries:
pip install plotly matplotlib
Error: "No bills found for issue"ā
Possible causes:
- Issue keyword too specific - Broaden search terms
- No legislation in that year - Try different year
- API quota exceeded - Wait for next month or upgrade
Solution: Customize keywords:
tracker.issue_keywords["fluoridation"]["ban"].append("anti-fluoride")
Map shows no dataā
Check:
- CSV file was created:
data/cache/legislation/{issue}_{year}.csv - CSV has rows with state_code values
- Categorization logic matched bills correctly
š” Tips & Best Practicesā
- Start broad, then refine - Use general keywords first, then add specific terms
- Cache aggressively - API calls are rate-limited, save results locally
- Update regularly - Legislation changes daily during session
- Verify categorization - Review sample bills to ensure accuracy
- Document keywords - Keep track of which keywords work best
- Share visualizations - Export maps as images for social media
šØ Color Scheme Referenceā
Default Colors (Customizable)ā
color_map = {
('ban', 'enacted'): '#D2691E', # Brown
('ban', 'introduced'): '#FFA500', # Orange
('ban', 'failed'): '#FFE4B5', # Moccasin
('restriction', 'enacted'): '#DAA520', # Goldenrod
('restriction', 'introduced'): '#FFD700', # Gold
('restriction', 'failed'): '#FFFFE0', # Light Yellow
('protection', 'enacted'): '#00008B', # Dark Blue
('protection', 'introduced'): '#4169E1', # Royal Blue
('protection', 'failed'): '#87CEEB', # Sky Blue
}
Custom Color Schemeā
# Update colors for your brand
tracker.color_map = {
('ban', 'enacted'): '#FF0000', # Red for enacted bans
('protection', 'enacted'): '#00FF00', # Green for enacted protections
}