ee-in commited on
Commit
f33c318
·
verified ·
1 Parent(s): 6fc6ce8

Upload 6 files

Browse files
HF_politico_AI.code-workspace ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "folders": [
3
+ {
4
+ "path": "."
5
+ }
6
+ ],
7
+ "settings": {}
8
+ }
app.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PoliticoAI – Hugging Face Space edition
3
+ Light-weight API that mirrors the full contract from the Production Guide.
4
+ Data: small synthetic sample so the demo works instantly.
5
+ """
6
+ from fastapi import FastAPI, HTTPException, UploadFile, File
7
+ from fastapi.staticfiles import StaticFiles
8
+ from fastapi.responses import JSONResponse
9
+ from pydantic import BaseModel
10
+ import pandas as pd
11
+ import numpy as np
12
+ from typing import List, Dict, Any
13
+
14
+ # ------------------------------------------------------------------
15
+ # 1. tiny synthetic dataset (mirrors the schema in the Guide)
16
+ # ------------------------------------------------------------------
17
+ PRECINCT_STORE: List[Dict[str, Any]] = [
18
+ {
19
+ "id": "P01",
20
+ "name": "Urban Ward 1",
21
+ "county": "Demo County",
22
+ "classification": "Urban",
23
+ "registered": 3500,
24
+ "latest_d_pct": 63.0,
25
+ "base_turnout": 0.70,
26
+ "results": {
27
+ 2020: {"d_pct": 65, "turnout": 72},
28
+ 2022: {"d_pct": 60, "turnout": 50},
29
+ 2024: {"d_pct": 63, "turnout": 70},
30
+ },
31
+ "demographics": {
32
+ "pct_college": 68,
33
+ "pct_under_35": 40,
34
+ "pct_35_64": 45,
35
+ "pct_65plus": 15,
36
+ "median_income": 48000,
37
+ "pct_white": 55,
38
+ "pct_black": 20,
39
+ "pct_hispanic": 20,
40
+ "pct_asian": 5,
41
+ },
42
+ "centroid": [-75.47, 40.61],
43
+ }
44
+ for i in range(1, 11) # 10 precincts
45
+ ]
46
+
47
+ for p in PRECINCT_STORE:
48
+ p["id"] = f"P{i:02d}"
49
+ p["name"] = f"Precinct {i:02d}"
50
+ p["latest_d_pct"] = 45 + np.random.normal(0, 5) # competitive
51
+ p["base_turnout"] = 0.65 + np.random.normal(0, 0.05)
52
+
53
+ # ------------------------------------------------------------------
54
+ # 2. FastAPI boiler-plate
55
+ # ------------------------------------------------------------------
56
+ app = FastAPI(title="PoliticoAI API", version="1.0")
57
+ app.mount("/", StaticFiles(directory="static", html=True), name="static")
58
+
59
+ # ------------------------------------------------------------------
60
+ # 3. Models (match the Production Guide contract)
61
+ # ------------------------------------------------------------------
62
+ class SimulationParams(BaseModel):
63
+ iterations: int = 10000
64
+ environment: str = "neutral"
65
+ polling_error: float = 3.5
66
+ turnout_variability: float = 5.0
67
+
68
+ class SegmentParams(BaseModel):
69
+ primary: str = "age"
70
+ secondary: str = "none"
71
+ metric: str = "size"
72
+
73
+ # ------------------------------------------------------------------
74
+ # 4. API endpoints (exact contract from Guide appendix)
75
+ # ------------------------------------------------------------------
76
+ @app.get("/api/health")
77
+ def health():
78
+ return {"status": "ok"}
79
+
80
+ @app.get("/api/precincts")
81
+ def get_precincts():
82
+ # strip heavy geometry if present, send only numbers
83
+ return [
84
+ {
85
+ "id": p["id"],
86
+ "name": p["name"],
87
+ "classification": p["classification"],
88
+ "registered_voters": p["registered"],
89
+ "latest_d_pct": p["latest_d_pct"],
90
+ "base_turnout": p["base_turnout"],
91
+ "demographics": p["demographics"],
92
+ }
93
+ for p in PRECINCT_STORE
94
+ ]
95
+
96
+ @app.post("/api/simulation/montecarlo")
97
+ def run_monte_carlo(params: SimulationParams):
98
+ env_map = {"neutral": 0, "d3": 3, "r3": -3, "d5": 5, "r5": -5}
99
+ env_shift = env_map.get(params.environment, 0)
100
+ margins = []
101
+ for _ in range(params.iterations):
102
+ env_noise = np.random.normal(env_shift, params.polling_error)
103
+ total_d, total_r = 0, 0
104
+ for pct in PRECINCT_STORE:
105
+ base_d = pct["latest_d_pct"] / 100
106
+ adj_d = np.clip(base_d + env_noise / 100, 0.05, 0.95)
107
+ turnout = np.clip(
108
+ np.random.normal(pct["base_turnout"], params.turnout_variability / 100),
109
+ 0.2, 0.95,
110
+ )
111
+ votes = int(pct["registered"] * turnout)
112
+ total_d += int(votes * adj_d)
113
+ total_r += votes - int(votes * adj_d)
114
+ margins.append((total_d - total_r) / (total_d + total_r) * 100)
115
+ margins = np.array(margins)
116
+ win_prob = float((margins > 0).mean() * 100)
117
+ return {
118
+ "win_prob": win_prob,
119
+ "expected_margin": float(margins.mean()),
120
+ "recount_prob": float((np.abs(margins) < 0.5).mean() * 100),
121
+ "margins": margins.tolist(),
122
+ "ci_95": [float(np.percentile(margins, 2.5)), float(np.percentile(margins, 97.5))],
123
+ }
124
+
125
+ @app.post("/api/demographics/segment")
126
+ def segment_demographics(params: SegmentParams):
127
+ # lightweight demo segmentation
128
+ segments = ["Under 35", "35-64", "65+"]
129
+ treemap = {
130
+ "labels": ["District"] + segments,
131
+ "parents": [""] + ["District"] * len(segments),
132
+ "values": [3500, 4000, 2500],
133
+ }
134
+ matrix = [
135
+ {"segment": f"Age: {seg}", "size": 3500 if seg == "Under 35" else 4000 if seg == "35-64" else 2500,
136
+ "turnout": "69%", "lean": "D+2", "persuadability": "High", "strategy": "Persuade"}
137
+ for seg in segments
138
+ ]
139
+ return {"treemap": treemap, "targeting_matrix": matrix}
140
+
141
+ @app.post("/api/precincts/analyze")
142
+ def analyze_precincts():
143
+ # minimal clustering demo
144
+ metrics = []
145
+ for p in PRECINCT_STORE:
146
+ pvi = p["latest_d_pct"] - 50
147
+ swing = np.random.uniform(1, 8)
148
+ to_delta = np.random.uniform(15, 30)
149
+ net = (swing + to_delta) * p["registered"] / 1000
150
+ metrics.append(
151
+ {
152
+ "id": p["id"],
153
+ "name": p["name"],
154
+ "pvi": round(pvi, 1),
155
+ "swingScore": round(swing, 1),
156
+ "turnoutDelta": round(to_delta, 1),
157
+ "net": round(net),
158
+ "tier": "High Swing" if swing > 4.5 else "Blue Base" if pvi > 0 else "Red Lean",
159
+ }
160
+ )
161
+ metrics = sorted(metrics, key=lambda x: x["net"], reverse=True)
162
+ return {"scatter_data": metrics, "ranking": metrics[:15], "table": metrics[:20]}
163
+
164
+ @app.post("/api/upload/results")
165
+ async def upload_results(file: UploadFile = File(...)):
166
+ # accept any CSV, store in memory (or persist to disk if you like)
167
+ df = pd.read_csv(file.file)
168
+ # here you would validate, clean, merge into PRECINCT_STORE, etc.
169
+ return {"rows": len(df), "columns": list(df.columns)}
170
+
171
+ # ------------------------------------------------------------------
172
+ # 5. CORS — Spaces needs this for browser access
173
+ # ------------------------------------------------------------------
174
+ from fastapi.middleware.cors import CORSMiddleware
175
+ app.add_middleware(
176
+ CORSMiddleware,
177
+ allow_origins=["*"], # restrict in prod
178
+ allow_methods=["*"],
179
+ allow_headers=["*"],
180
+ )
181
+
182
+ # ------------------------------------------------------------------
183
+ # 6. Entry-point guard (lets uvicorn import without running)
184
+ # ------------------------------------------------------------------
185
+ if __name__ == "__main__":
186
+ import uvicorn
187
+ uvicorn.run(app, host="0.0.0.0", port=7860)
dockerfile ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ # system deps for geopandas / spatial libs (optional but recommended)
4
+ RUN apt-get update && apt-get install -y \
5
+ libgdal-dev libspatialindex-dev gcc g++ \
6
+ && rm -rf /var/lib/apt/lists/*
7
+
8
+ WORKDIR /app
9
+
10
+ COPY requirements.txt .
11
+ RUN pip install --no-cache-dir -r requirements.txt
12
+
13
+ # back-end code
14
+ COPY app.py .
15
+
16
+ # front-end GUI
17
+ COPY static ./static
18
+
19
+ # expose port that Spaces expects
20
+ EXPOSE 7860
21
+
22
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.30.0
3
+ pandas==2.2.0
4
+ numpy==2.0.0
5
+ scipy==1.14.0
6
+ scikit-learn==1.5.0
7
+ plotly==5.22.0
8
+ python-multipart==0.0.9
static/mnt_politicoai_gui.html ADDED
@@ -0,0 +1,1591 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>PoliticoAI - Campaign Strategy Platform</title>
7
+
8
+ <!-- Bootstrap CSS -->
9
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
10
+ <!-- Bootstrap Icons -->
11
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
12
+ <!-- Plotly -->
13
+ <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
14
+
15
+ <style>
16
+ :root {
17
+ --primary-blue: #1e40af;
18
+ --primary-red: #dc2626;
19
+ --sidebar-width: 260px;
20
+ --header-height: 60px;
21
+ }
22
+
23
+ body {
24
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
25
+ background: #f8fafc;
26
+ overflow-x: hidden;
27
+ }
28
+
29
+ /* Header */
30
+ .app-header {
31
+ position: fixed;
32
+ top: 0;
33
+ left: 0;
34
+ right: 0;
35
+ height: var(--header-height);
36
+ background: linear-gradient(135deg, var(--primary-blue) 0%, #3b82f6 100%);
37
+ color: white;
38
+ display: flex;
39
+ align-items: center;
40
+ padding: 0 24px;
41
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
42
+ z-index: 1000;
43
+ }
44
+
45
+ .app-header h1 {
46
+ font-size: 24px;
47
+ font-weight: 700;
48
+ margin: 0;
49
+ }
50
+
51
+ .app-header .badge {
52
+ margin-left: 12px;
53
+ font-size: 11px;
54
+ font-weight: 600;
55
+ }
56
+
57
+ /* Sidebar */
58
+ .sidebar {
59
+ position: fixed;
60
+ top: var(--header-height);
61
+ left: 0;
62
+ width: var(--sidebar-width);
63
+ height: calc(100vh - var(--header-height));
64
+ background: white;
65
+ border-right: 1px solid #e2e8f0;
66
+ overflow-y: auto;
67
+ z-index: 999;
68
+ }
69
+
70
+ .nav-section {
71
+ padding: 24px 0;
72
+ border-bottom: 1px solid #e2e8f0;
73
+ }
74
+
75
+ .nav-section-title {
76
+ padding: 0 20px 8px;
77
+ font-size: 11px;
78
+ font-weight: 700;
79
+ text-transform: uppercase;
80
+ color: #64748b;
81
+ letter-spacing: 0.5px;
82
+ }
83
+
84
+ .nav-item {
85
+ padding: 10px 20px;
86
+ cursor: pointer;
87
+ display: flex;
88
+ align-items: center;
89
+ color: #475569;
90
+ transition: all 0.2s;
91
+ }
92
+
93
+ .nav-item:hover {
94
+ background: #f1f5f9;
95
+ color: var(--primary-blue);
96
+ }
97
+
98
+ .nav-item.active {
99
+ background: #eff6ff;
100
+ color: var(--primary-blue);
101
+ border-left: 3px solid var(--primary-blue);
102
+ }
103
+
104
+ .nav-item i {
105
+ width: 20px;
106
+ margin-right: 12px;
107
+ font-size: 18px;
108
+ }
109
+
110
+ /* Main Content */
111
+ .main-content {
112
+ margin-left: var(--sidebar-width);
113
+ margin-top: var(--header-height);
114
+ padding: 32px;
115
+ min-height: calc(100vh - var(--header-height));
116
+ }
117
+
118
+ .content-section {
119
+ display: none;
120
+ }
121
+
122
+ .content-section.active {
123
+ display: block;
124
+ }
125
+
126
+ /* Cards */
127
+ .card {
128
+ border: none;
129
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
130
+ border-radius: 8px;
131
+ margin-bottom: 24px;
132
+ }
133
+
134
+ .card-header {
135
+ background: white;
136
+ border-bottom: 1px solid #e2e8f0;
137
+ padding: 20px 24px;
138
+ font-weight: 600;
139
+ color: #1e293b;
140
+ }
141
+
142
+ .card-body {
143
+ padding: 24px;
144
+ }
145
+
146
+ /* Metrics Grid */
147
+ .metrics-grid {
148
+ display: grid;
149
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
150
+ gap: 20px;
151
+ margin-bottom: 32px;
152
+ }
153
+
154
+ .metric-card {
155
+ background: white;
156
+ padding: 24px;
157
+ border-radius: 8px;
158
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
159
+ border-left: 4px solid;
160
+ }
161
+
162
+ .metric-card.blue {
163
+ border-left-color: var(--primary-blue);
164
+ }
165
+
166
+ .metric-card.red {
167
+ border-left-color: var(--primary-red);
168
+ }
169
+
170
+ .metric-card.purple {
171
+ border-left-color: #9333ea;
172
+ }
173
+
174
+ .metric-card.green {
175
+ border-left-color: #16a34a;
176
+ }
177
+
178
+ .metric-label {
179
+ font-size: 13px;
180
+ font-weight: 600;
181
+ color: #64748b;
182
+ text-transform: uppercase;
183
+ letter-spacing: 0.5px;
184
+ }
185
+
186
+ .metric-value {
187
+ font-size: 32px;
188
+ font-weight: 700;
189
+ color: #1e293b;
190
+ margin-top: 8px;
191
+ }
192
+
193
+ .metric-change {
194
+ font-size: 13px;
195
+ margin-top: 8px;
196
+ }
197
+
198
+ .metric-change.positive {
199
+ color: #16a34a;
200
+ }
201
+
202
+ .metric-change.negative {
203
+ color: #dc2626;
204
+ }
205
+
206
+ /* Forms */
207
+ .form-label {
208
+ font-weight: 600;
209
+ font-size: 14px;
210
+ color: #334155;
211
+ margin-bottom: 8px;
212
+ }
213
+
214
+ .form-control, .form-select {
215
+ border: 1px solid #cbd5e1;
216
+ border-radius: 6px;
217
+ padding: 10px 14px;
218
+ }
219
+
220
+ .form-control:focus, .form-select:focus {
221
+ border-color: var(--primary-blue);
222
+ box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
223
+ }
224
+
225
+ /* Buttons */
226
+ .btn {
227
+ padding: 10px 20px;
228
+ border-radius: 6px;
229
+ font-weight: 600;
230
+ transition: all 0.2s;
231
+ }
232
+
233
+ .btn-primary {
234
+ background: var(--primary-blue);
235
+ border: none;
236
+ }
237
+
238
+ .btn-primary:hover {
239
+ background: #1e3a8a;
240
+ transform: translateY(-1px);
241
+ }
242
+
243
+ .btn-success {
244
+ background: #16a34a;
245
+ border: none;
246
+ }
247
+
248
+ .btn-danger {
249
+ background: var(--primary-red);
250
+ border: none;
251
+ }
252
+
253
+ /* Tables */
254
+ .table {
255
+ font-size: 14px;
256
+ }
257
+
258
+ .table thead th {
259
+ background: #f8fafc;
260
+ color: #475569;
261
+ font-weight: 600;
262
+ border-bottom: 2px solid #e2e8f0;
263
+ text-transform: uppercase;
264
+ font-size: 12px;
265
+ letter-spacing: 0.5px;
266
+ }
267
+
268
+ .table tbody tr:hover {
269
+ background: #f8fafc;
270
+ }
271
+
272
+ /* Visualization Container */
273
+ .viz-container {
274
+ background: white;
275
+ border-radius: 8px;
276
+ padding: 24px;
277
+ margin-top: 24px;
278
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
279
+ }
280
+
281
+ /* Alerts */
282
+ .alert {
283
+ border: none;
284
+ border-radius: 6px;
285
+ border-left: 4px solid;
286
+ }
287
+
288
+ .alert-info {
289
+ border-left-color: #3b82f6;
290
+ background: #eff6ff;
291
+ color: #1e40af;
292
+ }
293
+
294
+ .alert-success {
295
+ border-left-color: #16a34a;
296
+ background: #f0fdf4;
297
+ color: #15803d;
298
+ }
299
+
300
+ .alert-warning {
301
+ border-left-color: #f59e0b;
302
+ background: #fffbeb;
303
+ color: #b45309;
304
+ }
305
+
306
+ /* Loading Spinner */
307
+ .spinner {
308
+ display: none;
309
+ text-align: center;
310
+ padding: 40px;
311
+ }
312
+
313
+ .spinner.active {
314
+ display: block;
315
+ }
316
+
317
+ /* Range Slider Custom Style */
318
+ input[type="range"] {
319
+ width: 100%;
320
+ height: 6px;
321
+ border-radius: 3px;
322
+ background: #e2e8f0;
323
+ outline: none;
324
+ }
325
+
326
+ input[type="range"]::-webkit-slider-thumb {
327
+ -webkit-appearance: none;
328
+ appearance: none;
329
+ width: 20px;
330
+ height: 20px;
331
+ border-radius: 50%;
332
+ background: var(--primary-blue);
333
+ cursor: pointer;
334
+ }
335
+
336
+ input[type="range"]::-moz-range-thumb {
337
+ width: 20px;
338
+ height: 20px;
339
+ border-radius: 50%;
340
+ background: var(--primary-blue);
341
+ cursor: pointer;
342
+ }
343
+
344
+ .slider-value {
345
+ display: inline-block;
346
+ min-width: 50px;
347
+ text-align: center;
348
+ font-weight: 600;
349
+ color: var(--primary-blue);
350
+ }
351
+
352
+ /* File Upload */
353
+ .file-upload {
354
+ border: 2px dashed #cbd5e1;
355
+ border-radius: 8px;
356
+ padding: 40px;
357
+ text-align: center;
358
+ background: #f8fafc;
359
+ cursor: pointer;
360
+ transition: all 0.2s;
361
+ }
362
+
363
+ .file-upload:hover {
364
+ border-color: var(--primary-blue);
365
+ background: #eff6ff;
366
+ }
367
+
368
+ .file-upload i {
369
+ font-size: 48px;
370
+ color: #94a3b8;
371
+ margin-bottom: 16px;
372
+ }
373
+
374
+ /* Progress Bar */
375
+ .progress {
376
+ height: 8px;
377
+ border-radius: 4px;
378
+ background: #e2e8f0;
379
+ }
380
+
381
+ .progress-bar {
382
+ background: var(--primary-blue);
383
+ border-radius: 4px;
384
+ }
385
+
386
+ /* Tabs */
387
+ .nav-tabs {
388
+ border-bottom: 2px solid #e2e8f0;
389
+ }
390
+
391
+ .nav-tabs .nav-link {
392
+ border: none;
393
+ color: #64748b;
394
+ font-weight: 600;
395
+ padding: 12px 24px;
396
+ }
397
+
398
+ .nav-tabs .nav-link.active {
399
+ color: var(--primary-blue);
400
+ border-bottom: 2px solid var(--primary-blue);
401
+ margin-bottom: -2px;
402
+ }
403
+ </style>
404
+ </head>
405
+ <body>
406
+ <!-- Header -->
407
+ <div class="app-header">
408
+ <h1><i class="bi bi-graph-up-arrow"></i> PoliticoAI</h1>
409
+ <span class="badge bg-light text-primary">Campaign Strategy Platform</span>
410
+ </div>
411
+
412
+ <!-- Sidebar Navigation -->
413
+ <div class="sidebar">
414
+ <div class="nav-section">
415
+ <div class="nav-section-title">Analytics</div>
416
+ <div class="nav-item active" data-section="dashboard">
417
+ <i class="bi bi-speedometer2"></i>
418
+ <span>Dashboard</span>
419
+ </div>
420
+ <div class="nav-item" data-section="demographics">
421
+ <i class="bi bi-people"></i>
422
+ <span>Demographics</span>
423
+ </div>
424
+ <div class="nav-item" data-section="precincts">
425
+ <i class="bi bi-map"></i>
426
+ <span>Precinct Analysis</span>
427
+ </div>
428
+ </div>
429
+
430
+ <div class="nav-section">
431
+ <div class="nav-section-title">Modeling</div>
432
+ <div class="nav-item" data-section="simulation">
433
+ <i class="bi bi-cpu"></i>
434
+ <span>Monte Carlo</span>
435
+ </div>
436
+ <div class="nav-item" data-section="scenarios">
437
+ <i class="bi bi-sliders"></i>
438
+ <span>Scenarios</span>
439
+ </div>
440
+ <div class="nav-item" data-section="forecasting">
441
+ <i class="bi bi-graph-up"></i>
442
+ <span>Forecasting</span>
443
+ </div>
444
+ </div>
445
+
446
+ <div class="nav-section">
447
+ <div class="nav-section-title">Strategy</div>
448
+ <div class="nav-item" data-section="targeting">
449
+ <i class="bi bi-bullseye"></i>
450
+ <span>Voter Targeting</span>
451
+ </div>
452
+ <div class="nav-item" data-section="gotv">
453
+ <i class="bi bi-megaphone"></i>
454
+ <span>GOTV Planning</span>
455
+ </div>
456
+ <div class="nav-item" data-section="messaging">
457
+ <i class="bi bi-chat-square-text"></i>
458
+ <span>Messaging</span>
459
+ </div>
460
+ </div>
461
+
462
+ <div class="nav-section">
463
+ <div class="nav-section-title">Data</div>
464
+ <div class="nav-item" data-section="upload">
465
+ <i class="bi bi-upload"></i>
466
+ <span>Data Upload</span>
467
+ </div>
468
+ <div class="nav-item" data-section="export">
469
+ <i class="bi bi-download"></i>
470
+ <span>Export</span>
471
+ </div>
472
+ </div>
473
+ </div>
474
+
475
+ <!-- Main Content -->
476
+ <div class="main-content">
477
+
478
+ <!-- Dashboard Section -->
479
+ <div id="dashboard" class="content-section active">
480
+ <h2>Campaign Dashboard</h2>
481
+ <p class="text-muted mb-4">Real-time overview of district performance and strategic metrics</p>
482
+
483
+ <div class="metrics-grid">
484
+ <div class="metric-card blue">
485
+ <div class="metric-label">Current Win Probability</div>
486
+ <div class="metric-value" id="win-prob">--</div>
487
+ <div class="metric-change positive" id="win-prob-change">
488
+ <i class="bi bi-arrow-up"></i> +5.2% vs. baseline
489
+ </div>
490
+ </div>
491
+
492
+ <div class="metric-card purple">
493
+ <div class="metric-label">Expected Margin</div>
494
+ <div class="metric-value" id="expected-margin">--</div>
495
+ <div class="metric-change" id="margin-change">
496
+ ±2.1% (95% CI)
497
+ </div>
498
+ </div>
499
+
500
+ <div class="metric-card green">
501
+ <div class="metric-label">High-Value Precincts</div>
502
+ <div class="metric-value" id="high-value-precincts">--</div>
503
+ <div class="metric-change">
504
+ <i class="bi bi-geo-alt"></i> Top quartile by net value
505
+ </div>
506
+ </div>
507
+
508
+ <div class="metric-card red">
509
+ <div class="metric-label">Voter Contact Target</div>
510
+ <div class="metric-value" id="contact-target">--</div>
511
+ <div class="metric-change">
512
+ <i class="bi bi-person-check"></i> Doors + phones
513
+ </div>
514
+ </div>
515
+ </div>
516
+
517
+ <div class="row">
518
+ <div class="col-lg-8">
519
+ <div class="card">
520
+ <div class="card-header">
521
+ <i class="bi bi-map-fill"></i> District Map - Partisan Lean (PVI)
522
+ </div>
523
+ <div class="card-body">
524
+ <div id="district-map" style="height: 400px;"></div>
525
+ </div>
526
+ </div>
527
+ </div>
528
+
529
+ <div class="col-lg-4">
530
+ <div class="card">
531
+ <div class="card-header">
532
+ <i class="bi bi-pie-chart-fill"></i> Demographic Breakdown
533
+ </div>
534
+ <div class="card-body">
535
+ <div id="demo-pie" style="height: 400px;"></div>
536
+ </div>
537
+ </div>
538
+ </div>
539
+ </div>
540
+
541
+ <div class="card">
542
+ <div class="card-header">
543
+ <i class="bi bi-graph-up"></i> Win Probability Over Time
544
+ </div>
545
+ <div class="card-body">
546
+ <div id="prob-timeline" style="height: 300px;"></div>
547
+ </div>
548
+ </div>
549
+ </div>
550
+
551
+ <!-- Demographics Section -->
552
+ <div id="demographics" class="content-section">
553
+ <h2>Demographic Segmentation</h2>
554
+ <p class="text-muted mb-4">Analyze voter universe by age, education, race, income, and geography</p>
555
+
556
+ <div class="card">
557
+ <div class="card-header">
558
+ <i class="bi bi-sliders"></i> Segmentation Controls
559
+ </div>
560
+ <div class="card-body">
561
+ <div class="row">
562
+ <div class="col-md-4">
563
+ <label class="form-label">Primary Dimension</label>
564
+ <select class="form-select" id="demo-primary">
565
+ <option value="age">Age Cohort</option>
566
+ <option value="education">Education Level</option>
567
+ <option value="race">Race/Ethnicity</option>
568
+ <option value="income">Income Quintile</option>
569
+ <option value="geography">Geography (Urban/Suburban/Rural)</option>
570
+ </select>
571
+ </div>
572
+ <div class="col-md-4">
573
+ <label class="form-label">Secondary Dimension</label>
574
+ <select class="form-select" id="demo-secondary">
575
+ <option value="none">None</option>
576
+ <option value="age">Age Cohort</option>
577
+ <option value="education">Education Level</option>
578
+ <option value="race">Race/Ethnicity</option>
579
+ <option value="income">Income Quintile</option>
580
+ </select>
581
+ </div>
582
+ <div class="col-md-4">
583
+ <label class="form-label">Metric to Display</label>
584
+ <select class="form-select" id="demo-metric">
585
+ <option value="size">Segment Size</option>
586
+ <option value="turnout">Turnout Rate</option>
587
+ <option value="lean">Partisan Lean</option>
588
+ <option value="persuadability">Persuadability</option>
589
+ </select>
590
+ </div>
591
+ </div>
592
+ <div class="mt-3">
593
+ <button class="btn btn-primary" onclick="generateDemoSegmentation()">
594
+ <i class="bi bi-play-fill"></i> Generate Segmentation
595
+ </button>
596
+ </div>
597
+ </div>
598
+ </div>
599
+
600
+ <div class="viz-container">
601
+ <div id="demo-treemap" style="height: 500px;"></div>
602
+ </div>
603
+
604
+ <div class="card">
605
+ <div class="card-header">
606
+ <i class="bi bi-table"></i> Targeting Matrix
607
+ </div>
608
+ <div class="card-body">
609
+ <div class="table-responsive">
610
+ <table class="table table-hover" id="targeting-matrix">
611
+ <thead>
612
+ <tr>
613
+ <th>Segment</th>
614
+ <th>Size</th>
615
+ <th>Turnout Rate</th>
616
+ <th>Partisan Lean</th>
617
+ <th>Persuadability</th>
618
+ <th>Strategy</th>
619
+ </tr>
620
+ </thead>
621
+ <tbody>
622
+ <tr>
623
+ <td colspan="6" class="text-center text-muted">
624
+ Run segmentation to populate table
625
+ </td>
626
+ </tr>
627
+ </tbody>
628
+ </table>
629
+ </div>
630
+ </div>
631
+ </div>
632
+ </div>
633
+
634
+ <!-- Precinct Analysis Section -->
635
+ <div id="precincts" class="content-section">
636
+ <h2>Precinct-Level Analysis</h2>
637
+ <p class="text-muted mb-4">Deep dive into precinct metrics, clustering, and strategic prioritization</p>
638
+
639
+ <div class="alert alert-info">
640
+ <i class="bi bi-info-circle"></i>
641
+ <strong>Analysis Level:</strong> This module computes PVI, swing scores, elasticity, turnout metrics, and strategic value for every precinct.
642
+ </div>
643
+
644
+ <div class="card">
645
+ <div class="card-header">
646
+ <i class="bi bi-gear"></i> Analysis Parameters
647
+ </div>
648
+ <div class="card-body">
649
+ <div class="row">
650
+ <div class="col-md-4">
651
+ <label class="form-label">Clustering Method</label>
652
+ <select class="form-select" id="cluster-method">
653
+ <option value="kmeans">K-Means</option>
654
+ <option value="hierarchical">Hierarchical (Ward)</option>
655
+ </select>
656
+ </div>
657
+ <div class="col-md-4">
658
+ <label class="form-label">Number of Clusters</label>
659
+ <select class="form-select" id="num-clusters">
660
+ <option value="4">4 Tiers</option>
661
+ <option value="5">5 Tiers</option>
662
+ <option value="6">6 Tiers</option>
663
+ <option value="8">8 Tiers</option>
664
+ </select>
665
+ </div>
666
+ <div class="col-md-4">
667
+ <label class="form-label">Priority Metric</label>
668
+ <select class="form-select" id="priority-metric">
669
+ <option value="net">Net Mobilization Score</option>
670
+ <option value="persuasion">Persuasion Value</option>
671
+ <option value="mobilization">Mobilization Value</option>
672
+ <option value="competitiveness">Competitiveness</option>
673
+ </select>
674
+ </div>
675
+ </div>
676
+ <div class="mt-3">
677
+ <button class="btn btn-primary" onclick="analyzePrecincts()">
678
+ <i class="bi bi-play-fill"></i> Run Precinct Analysis
679
+ </button>
680
+ </div>
681
+ </div>
682
+ </div>
683
+
684
+ <div class="row">
685
+ <div class="col-lg-6">
686
+ <div class="viz-container">
687
+ <h5>PVI vs. Swing Score (by Cluster)</h5>
688
+ <div id="precinct-scatter" style="height: 400px;"></div>
689
+ </div>
690
+ </div>
691
+ <div class="col-lg-6">
692
+ <div class="viz-container">
693
+ <h5>Precinct Priority Ranking</h5>
694
+ <div id="precinct-ranking" style="height: 400px;"></div>
695
+ </div>
696
+ </div>
697
+ </div>
698
+
699
+ <div class="card">
700
+ <div class="card-header">
701
+ <i class="bi bi-list-ol"></i> Top 20 Precincts by Strategic Value
702
+ </div>
703
+ <div class="card-body">
704
+ <div class="table-responsive">
705
+ <table class="table table-sm" id="precinct-table">
706
+ <thead>
707
+ <tr>
708
+ <th>Rank</th>
709
+ <th>Precinct</th>
710
+ <th>PVI</th>
711
+ <th>Swing</th>
712
+ <th>TO Delta</th>
713
+ <th>Net Value</th>
714
+ <th>Tier</th>
715
+ </tr>
716
+ </thead>
717
+ <tbody>
718
+ <tr>
719
+ <td colspan="7" class="text-center text-muted">
720
+ Run analysis to populate table
721
+ </td>
722
+ </tr>
723
+ </tbody>
724
+ </table>
725
+ </div>
726
+ </div>
727
+ </div>
728
+ </div>
729
+
730
+ <!-- Monte Carlo Simulation Section -->
731
+ <div id="simulation" class="content-section">
732
+ <h2>Monte Carlo Simulation</h2>
733
+ <p class="text-muted mb-4">Run thousands of election simulations to estimate win probability and margin distribution</p>
734
+
735
+ <div class="card">
736
+ <div class="card-header">
737
+ <i class="bi bi-sliders"></i> Simulation Parameters
738
+ </div>
739
+ <div class="card-body">
740
+ <div class="row">
741
+ <div class="col-md-6">
742
+ <label class="form-label">Number of Iterations</label>
743
+ <select class="form-select" id="mc-iterations">
744
+ <option value="1000">1,000 (Quick)</option>
745
+ <option value="5000">5,000 (Standard)</option>
746
+ <option value="10000" selected>10,000 (Recommended)</option>
747
+ <option value="50000">50,000 (Tight Race)</option>
748
+ </select>
749
+ </div>
750
+ <div class="col-md-6">
751
+ <label class="form-label">Baseline Environment</label>
752
+ <select class="form-select" id="mc-environment">
753
+ <option value="neutral">Neutral / Even</option>
754
+ <option value="d3">D+3 Environment</option>
755
+ <option value="r3">R+3 Environment</option>
756
+ <option value="d5">D+5 Wave</option>
757
+ <option value="r5">R+5 Wave</option>
758
+ </select>
759
+ </div>
760
+ </div>
761
+
762
+ <div class="row mt-3">
763
+ <div class="col-md-6">
764
+ <label class="form-label">
765
+ Polling Error (σ): <span class="slider-value" id="polling-error-val">3.5</span>%
766
+ </label>
767
+ <input type="range" id="polling-error" min="1" max="6" step="0.5" value="3.5"
768
+ oninput="document.getElementById('polling-error-val').textContent = this.value">
769
+ </div>
770
+ <div class="col-md-6">
771
+ <label class="form-label">
772
+ Turnout Variability: <span class="slider-value" id="turnout-var-val">5</span>%
773
+ </label>
774
+ <input type="range" id="turnout-var" min="2" max="10" step="1" value="5"
775
+ oninput="document.getElementById('turnout-var-val').textContent = this.value">
776
+ </div>
777
+ </div>
778
+
779
+ <div class="mt-4">
780
+ <button class="btn btn-primary btn-lg" onclick="runMonteCarloSimulation()">
781
+ <i class="bi bi-play-circle-fill"></i> Run Simulation
782
+ </button>
783
+ <button class="btn btn-secondary" onclick="resetSimulation()">
784
+ <i class="bi bi-arrow-clockwise"></i> Reset
785
+ </button>
786
+ </div>
787
+
788
+ <div id="mc-spinner" class="spinner">
789
+ <div class="spinner-border text-primary" role="status"></div>
790
+ <p class="mt-3">Running simulation...</p>
791
+ </div>
792
+ </div>
793
+ </div>
794
+
795
+ <div class="row">
796
+ <div class="col-lg-4">
797
+ <div class="metric-card blue">
798
+ <div class="metric-label">Win Probability</div>
799
+ <div class="metric-value" id="mc-win-prob">--</div>
800
+ </div>
801
+ </div>
802
+ <div class="col-lg-4">
803
+ <div class="metric-card purple">
804
+ <div class="metric-label">Expected Margin</div>
805
+ <div class="metric-value" id="mc-margin">--</div>
806
+ </div>
807
+ </div>
808
+ <div class="col-lg-4">
809
+ <div class="metric-card green">
810
+ <div class="metric-label">Recount Probability</div>
811
+ <div class="metric-value" id="mc-recount">--</div>
812
+ </div>
813
+ </div>
814
+ </div>
815
+
816
+ <div class="viz-container">
817
+ <h5>Margin Distribution (10,000 Simulations)</h5>
818
+ <div id="mc-histogram" style="height: 400px;"></div>
819
+ </div>
820
+ </div>
821
+
822
+ <!-- Scenarios Section -->
823
+ <div id="scenarios" class="content-section">
824
+ <h2>Scenario Builder</h2>
825
+ <p class="text-muted mb-4">Model "what if" scenarios with demographic shifts, turnout changes, and third-party effects</p>
826
+
827
+ <div class="card">
828
+ <div class="card-header">
829
+ <i class="bi bi-plus-circle"></i> Build Scenario
830
+ </div>
831
+ <div class="card-body">
832
+ <div class="row">
833
+ <div class="col-md-6">
834
+ <label class="form-label">Scenario Name</label>
835
+ <input type="text" class="form-control" id="scenario-name" placeholder="e.g., High Youth Turnout">
836
+ </div>
837
+ <div class="col-md-6">
838
+ <label class="form-label">Compare Against</label>
839
+ <select class="form-select" id="scenario-baseline">
840
+ <option value="2024">2024 Baseline</option>
841
+ <option value="2022">2022 Midterm</option>
842
+ <option value="2020">2020 Presidential</option>
843
+ </select>
844
+ </div>
845
+ </div>
846
+
847
+ <hr class="my-4">
848
+
849
+ <h6 class="mb-3">Turnout Adjustments (by demographic)</h6>
850
+ <div class="row">
851
+ <div class="col-md-6">
852
+ <label class="form-label">
853
+ 18-29 Turnout: <span class="slider-value" id="turnout-18-29-val">0</span>%
854
+ </label>
855
+ <input type="range" id="turnout-18-29" min="-30" max="30" step="5" value="0"
856
+ oninput="document.getElementById('turnout-18-29-val').textContent = (this.value > 0 ? '+' : '') + this.value">
857
+ </div>
858
+ <div class="col-md-6">
859
+ <label class="form-label">
860
+ College+ Turnout: <span class="slider-value" id="turnout-college-val">0</span>%
861
+ </label>
862
+ <input type="range" id="turnout-college" min="-30" max="30" step="5" value="0"
863
+ oninput="document.getElementById('turnout-college-val').textContent = (this.value > 0 ? '+' : '') + this.value">
864
+ </div>
865
+ </div>
866
+
867
+ <h6 class="mb-3 mt-4">Partisan Shifts (by demographic)</h6>
868
+ <div class="row">
869
+ <div class="col-md-6">
870
+ <label class="form-label">
871
+ Hispanic D Margin: <span class="slider-value" id="hispanic-shift-val">0</span> pts
872
+ </label>
873
+ <input type="range" id="hispanic-shift" min="-15" max="15" step="1" value="0"
874
+ oninput="document.getElementById('hispanic-shift-val').textContent = (this.value > 0 ? '+' : '') + this.value">
875
+ </div>
876
+ <div class="col-md-6">
877
+ <label class="form-label">
878
+ Suburban Women D Margin: <span class="slider-value" id="subwomen-shift-val">0</span> pts
879
+ </label>
880
+ <input type="range" id="subwomen-shift" min="-15" max="15" step="1" value="0"
881
+ oninput="document.getElementById('subwomen-shift-val').textContent = (this.value > 0 ? '+' : '') + this.value">
882
+ </div>
883
+ </div>
884
+
885
+ <div class="mt-4">
886
+ <button class="btn btn-success" onclick="runScenario()">
887
+ <i class="bi bi-calculator"></i> Calculate Scenario
888
+ </button>
889
+ </div>
890
+ </div>
891
+ </div>
892
+
893
+ <div class="card">
894
+ <div class="card-header">
895
+ <i class="bi bi-bar-chart-line"></i> Scenario Comparison
896
+ </div>
897
+ <div class="card-body">
898
+ <div id="scenario-comparison" style="height: 400px;"></div>
899
+ </div>
900
+ </div>
901
+ </div>
902
+
903
+ <!-- Voter Targeting Section -->
904
+ <div id="targeting" class="content-section">
905
+ <h2>Voter Targeting</h2>
906
+ <p class="text-muted mb-4">Build persuasion and mobilization universes with data-driven segment prioritization</p>
907
+
908
+ <div class="card">
909
+ <div class="card-header">
910
+ <i class="bi bi-funnel"></i> Build Universe
911
+ </div>
912
+ <div class="card-body">
913
+ <div class="row">
914
+ <div class="col-md-4">
915
+ <label class="form-label">Universe Type</label>
916
+ <select class="form-select" id="universe-type">
917
+ <option value="persuasion">Persuasion</option>
918
+ <option value="mobilization">Mobilization</option>
919
+ <option value="combined">Combined</option>
920
+ </select>
921
+ </div>
922
+ <div class="col-md-4">
923
+ <label class="form-label">Competitiveness Threshold</label>
924
+ <select class="form-select" id="comp-threshold">
925
+ <option value="0.7">High (>70% competitive)</option>
926
+ <option value="0.5" selected>Medium (>50%)</option>
927
+ <option value="0.3">Low (>30%)</option>
928
+ </select>
929
+ </div>
930
+ <div class="col-md-4">
931
+ <label class="form-label">Min Support Score</label>
932
+ <select class="form-select" id="support-score">
933
+ <option value="3">Lean Support (3+)</option>
934
+ <option value="4" selected>Likely Support (4+)</option>
935
+ <option value="5">Strong Support (5)</option>
936
+ </select>
937
+ </div>
938
+ </div>
939
+ <div class="mt-3">
940
+ <button class="btn btn-primary" onclick="buildUniverse()">
941
+ <i class="bi bi-play-fill"></i> Generate Universe
942
+ </button>
943
+ </div>
944
+ </div>
945
+ </div>
946
+
947
+ <div class="metrics-grid">
948
+ <div class="metric-card blue">
949
+ <div class="metric-label">Universe Size</div>
950
+ <div class="metric-value" id="universe-size">--</div>
951
+ </div>
952
+ <div class="metric-card purple">
953
+ <div class="metric-label">Contact Rate Goal</div>
954
+ <div class="metric-value" id="contact-rate">--</div>
955
+ </div>
956
+ <div class="metric-card green">
957
+ <div class="metric-label">Estimated Vote Yield</div>
958
+ <div class="metric-value" id="vote-yield">--</div>
959
+ </div>
960
+ </div>
961
+
962
+ <div class="card">
963
+ <div class="card-header">
964
+ <i class="bi bi-people-fill"></i> Segment Performance
965
+ </div>
966
+ <div class="card-body">
967
+ <div id="segment-performance" style="height: 400px;"></div>
968
+ </div>
969
+ </div>
970
+ </div>
971
+
972
+ <!-- GOTV Section -->
973
+ <div id="gotv" class="content-section">
974
+ <h2>GOTV Planning</h2>
975
+ <p class="text-muted mb-4">Optimize canvass turf, phone banks, and volunteer deployment</p>
976
+
977
+ <div class="alert alert-warning">
978
+ <i class="bi bi-exclamation-triangle"></i>
979
+ <strong>GOTV Window:</strong> Most effective in final 10-14 days. Focus on identified supporters only.
980
+ </div>
981
+
982
+ <div class="card">
983
+ <div class="card-header">
984
+ <i class="bi bi-calendar-check"></i> Resource Allocation
985
+ </div>
986
+ <div class="card-body">
987
+ <div class="row">
988
+ <div class="col-md-4">
989
+ <label class="form-label">Volunteer Hours Available</label>
990
+ <input type="number" class="form-control" id="volunteer-hours" value="500">
991
+ </div>
992
+ <div class="col-md-4">
993
+ <label class="form-label">Phone Bank Capacity</label>
994
+ <input type="number" class="form-control" id="phone-capacity" value="10000">
995
+ </div>
996
+ <div class="col-md-4">
997
+ <label class="form-label">Days Until Election</label>
998
+ <input type="number" class="form-control" id="days-to-election" value="14">
999
+ </div>
1000
+ </div>
1001
+
1002
+ <div class="row mt-3">
1003
+ <div class="col-md-6">
1004
+ <label class="form-label">Contact Mix</label>
1005
+ <select class="form-select" id="contact-mix">
1006
+ <option value="balanced">Balanced (50/50 doors/phones)</option>
1007
+ <option value="door-heavy">Door-Heavy (70/30)</option>
1008
+ <option value="phone-heavy">Phone-Heavy (30/70)</option>
1009
+ </select>
1010
+ </div>
1011
+ <div class="col-md-6">
1012
+ <label class="form-label">Priority Mode</label>
1013
+ <select class="form-select" id="priority-mode">
1014
+ <option value="net">Net Value (Balanced)</option>
1015
+ <option value="persuasion">Persuasion Focus</option>
1016
+ <option value="mobilization">Turnout Focus</option>
1017
+ </select>
1018
+ </div>
1019
+ </div>
1020
+
1021
+ <div class="mt-4">
1022
+ <button class="btn btn-success btn-lg" onclick="optimizeTurf()">
1023
+ <i class="bi bi-geo-alt-fill"></i> Optimize Turf Cutting
1024
+ </button>
1025
+ </div>
1026
+ </div>
1027
+ </div>
1028
+
1029
+ <div class="card">
1030
+ <div class="card-header">
1031
+ <i class="bi bi-map"></i> Recommended Precinct Allocation
1032
+ </div>
1033
+ <div class="card-body">
1034
+ <div class="table-responsive">
1035
+ <table class="table" id="turf-allocation">
1036
+ <thead>
1037
+ <tr>
1038
+ <th>Priority</th>
1039
+ <th>Precinct</th>
1040
+ <th>Doors</th>
1041
+ <th>Phone Attempts</th>
1042
+ <th>Vol. Hours</th>
1043
+ <th>Est. Votes</th>
1044
+ </tr>
1045
+ </thead>
1046
+ <tbody>
1047
+ <tr>
1048
+ <td colspan="6" class="text-center text-muted">
1049
+ Run optimization to generate allocation
1050
+ </td>
1051
+ </tr>
1052
+ </tbody>
1053
+ </table>
1054
+ </div>
1055
+ </div>
1056
+ </div>
1057
+ </div>
1058
+
1059
+ <!-- Messaging Section -->
1060
+ <div id="messaging" class="content-section">
1061
+ <h2>Messaging Strategy</h2>
1062
+ <p class="text-muted mb-4">Issue salience mapping and message-to-market fit analysis</p>
1063
+
1064
+ <div class="card">
1065
+ <div class="card-header">
1066
+ <i class="bi bi-list-check"></i> Issue Priorities by Segment
1067
+ </div>
1068
+ <div class="card-body">
1069
+ <div class="table-responsive">
1070
+ <table class="table">
1071
+ <thead>
1072
+ <tr>
1073
+ <th>Issue</th>
1074
+ <th>Young Urban</th>
1075
+ <th>Suburban Swing</th>
1076
+ <th>Rural Senior</th>
1077
+ <th>Overall</th>
1078
+ </tr>
1079
+ </thead>
1080
+ <tbody>
1081
+ <tr>
1082
+ <td><strong>Economy/Jobs</strong></td>
1083
+ <td><span class="badge bg-warning">Medium</span></td>
1084
+ <td><span class="badge bg-danger">Very High</span></td>
1085
+ <td><span class="badge bg-danger">Very High</span></td>
1086
+ <td><span class="badge bg-danger">Very High</span></td>
1087
+ </tr>
1088
+ <tr>
1089
+ <td><strong>Healthcare</strong></td>
1090
+ <td><span class="badge bg-success">High</span></td>
1091
+ <td><span class="badge bg-success">High</span></td>
1092
+ <td><span class="badge bg-danger">Very High</span></td>
1093
+ <td><span class="badge bg-success">High</span></td>
1094
+ </tr>
1095
+ <tr>
1096
+ <td><strong>Abortion/Reproductive Rights</strong></td>
1097
+ <td><span class="badge bg-danger">Very High</span></td>
1098
+ <td><span class="badge bg-success">High</span></td>
1099
+ <td><span class="badge bg-secondary">Low</span></td>
1100
+ <td><span class="badge bg-success">High</span></td>
1101
+ </tr>
1102
+ <tr>
1103
+ <td><strong>Immigration</strong></td>
1104
+ <td><span class="badge bg-secondary">Low</span></td>
1105
+ <td><span class="badge bg-warning">Medium</span></td>
1106
+ <td><span class="badge bg-danger">Very High</span></td>
1107
+ <td><span class="badge bg-success">High</span></td>
1108
+ </tr>
1109
+ <tr>
1110
+ <td><strong>Climate/Environment</strong></td>
1111
+ <td><span class="badge bg-danger">Very High</span></td>
1112
+ <td><span class="badge bg-warning">Medium</span></td>
1113
+ <td><span class="badge bg-secondary">Low</span></td>
1114
+ <td><span class="badge bg-warning">Medium</span></td>
1115
+ </tr>
1116
+ <tr>
1117
+ <td><strong>Education</strong></td>
1118
+ <td><span class="badge bg-warning">Medium</span></td>
1119
+ <td><span class="badge bg-danger">Very High</span></td>
1120
+ <td><span class="badge bg-warning">Medium</span></td>
1121
+ <td><span class="badge bg-success">High</span></td>
1122
+ </tr>
1123
+ </tbody>
1124
+ </table>
1125
+ </div>
1126
+ </div>
1127
+ </div>
1128
+
1129
+ <div class="alert alert-info">
1130
+ <i class="bi bi-lightbulb"></i>
1131
+ <strong>Strategy Recommendation:</strong> Lead with economy/jobs for suburban swing voters. Layer in healthcare for seniors. Use abortion rights to mobilize young urban base. Avoid immigration emphasis with young voters.
1132
+ </div>
1133
+ </div>
1134
+
1135
+ <!-- Forecasting Section -->
1136
+ <div id="forecasting" class="content-section">
1137
+ <h2>Election Forecasting</h2>
1138
+ <p class="text-muted mb-4">Bayesian model with polling integration and early vote tracking</p>
1139
+
1140
+ <div class="card">
1141
+ <div class="card-header">
1142
+ <i class="bi bi-graph-up"></i> Model Inputs
1143
+ </div>
1144
+ <div class="card-body">
1145
+ <ul class="nav nav-tabs" role="tablist">
1146
+ <li class="nav-item">
1147
+ <a class="nav-link active" data-bs-toggle="tab" href="#fundamentals-tab">Fundamentals</a>
1148
+ </li>
1149
+ <li class="nav-item">
1150
+ <a class="nav-link" data-bs-toggle="tab" href="#polling-tab">Polling</a>
1151
+ </li>
1152
+ <li class="nav-item">
1153
+ <a class="nav-link" data-bs-toggle="tab" href="#early-vote-tab">Early Vote</a>
1154
+ </li>
1155
+ </ul>
1156
+
1157
+ <div class="tab-content mt-3">
1158
+ <div id="fundamentals-tab" class="tab-pane fade show active">
1159
+ <div class="row">
1160
+ <div class="col-md-6">
1161
+ <label class="form-label">Incumbent Party</label>
1162
+ <select class="form-select">
1163
+ <option>Democrat</option>
1164
+ <option>Republican</option>
1165
+ <option>Open Seat</option>
1166
+ </select>
1167
+ </div>
1168
+ <div class="col-md-6">
1169
+ <label class="form-label">Generic Ballot (D+/-)</label>
1170
+ <input type="number" class="form-control" value="0" step="0.5">
1171
+ </div>
1172
+ </div>
1173
+ <div class="row mt-3">
1174
+ <div class="col-md-6">
1175
+ <label class="form-label">Presidential Approval</label>
1176
+ <input type="number" class="form-control" value="45" step="1">
1177
+ </div>
1178
+ <div class="col-md-6">
1179
+ <label class="form-label">Right Track/Wrong Track</label>
1180
+ <input type="number" class="form-control" value="35" step="1">
1181
+ </div>
1182
+ </div>
1183
+ </div>
1184
+
1185
+ <div id="polling-tab" class="tab-pane fade">
1186
+ <p class="text-muted">Add recent polls to update the forecast</p>
1187
+ <div class="row">
1188
+ <div class="col-md-4">
1189
+ <label class="form-label">D Margin</label>
1190
+ <input type="number" class="form-control" step="0.5">
1191
+ </div>
1192
+ <div class="col-md-4">
1193
+ <label class="form-label">Sample Size</label>
1194
+ <input type="number" class="form-control">
1195
+ </div>
1196
+ <div class="col-md-4">
1197
+ <label class="form-label">Pollster Rating</label>
1198
+ <select class="form-select">
1199
+ <option>A+</option>
1200
+ <option>A</option>
1201
+ <option>B</option>
1202
+ <option>C</option>
1203
+ </select>
1204
+ </div>
1205
+ </div>
1206
+ <button class="btn btn-sm btn-primary mt-2">
1207
+ <i class="bi bi-plus"></i> Add Poll
1208
+ </button>
1209
+ </div>
1210
+
1211
+ <div id="early-vote-tab" class="tab-pane fade">
1212
+ <p class="text-muted">Track early/mail ballots returned</p>
1213
+ <div class="row">
1214
+ <div class="col-md-6">
1215
+ <label class="form-label">D Early Vote Share (%)</label>
1216
+ <input type="number" class="form-control" step="0.5">
1217
+ </div>
1218
+ <div class="col-md-6">
1219
+ <label class="form-label">% of Expected Turnout</label>
1220
+ <input type="number" class="form-control" step="1">
1221
+ </div>
1222
+ </div>
1223
+ </div>
1224
+ </div>
1225
+ </div>
1226
+ </div>
1227
+
1228
+ <div class="card">
1229
+ <div class="card-header">
1230
+ <i class="bi bi-speedometer"></i> Current Forecast
1231
+ </div>
1232
+ <div class="card-body">
1233
+ <div class="row text-center">
1234
+ <div class="col-md-4">
1235
+ <h1 class="text-primary">52.3%</h1>
1236
+ <p class="text-muted">Win Probability</p>
1237
+ </div>
1238
+ <div class="col-md-4">
1239
+ <h1>D+1.8</h1>
1240
+ <p class="text-muted">Expected Margin</p>
1241
+ </div>
1242
+ <div class="col-md-4">
1243
+ <h1 class="text-success">85%</h1>
1244
+ <p class="text-muted">Model Confidence</p>
1245
+ </div>
1246
+ </div>
1247
+ <div class="progress mt-3" style="height: 30px;">
1248
+ <div class="progress-bar bg-primary" style="width: 52.3%">Democrat 52.3%</div>
1249
+ <div class="progress-bar bg-danger" style="width: 47.7%">Republican 47.7%</div>
1250
+ </div>
1251
+ </div>
1252
+ </div>
1253
+ </div>
1254
+
1255
+ <!-- Data Upload Section -->
1256
+ <div id="upload" class="content-section">
1257
+ <h2>Data Upload & Management</h2>
1258
+ <p class="text-muted mb-4">Import voter files, election results, demographics, and precinct shapefiles</p>
1259
+
1260
+ <div class="row">
1261
+ <div class="col-lg-6">
1262
+ <div class="card">
1263
+ <div class="card-header">
1264
+ <i class="bi bi-file-earmark-arrow-up"></i> Upload Election Results
1265
+ </div>
1266
+ <div class="card-body">
1267
+ <div class="file-upload">
1268
+ <i class="bi bi-cloud-upload"></i>
1269
+ <h5>Drag & Drop or Click to Upload</h5>
1270
+ <p class="text-muted">CSV, Excel, TXT (precinct-level results)</p>
1271
+ <input type="file" class="d-none" id="results-file" accept=".csv,.xlsx,.txt">
1272
+ <button class="btn btn-primary mt-2" onclick="document.getElementById('results-file').click()">
1273
+ Select File
1274
+ </button>
1275
+ </div>
1276
+ </div>
1277
+ </div>
1278
+ </div>
1279
+
1280
+ <div class="col-lg-6">
1281
+ <div class="card">
1282
+ <div class="card-header">
1283
+ <i class="bi bi-people"></i> Upload Voter File
1284
+ </div>
1285
+ <div class="card-body">
1286
+ <div class="file-upload">
1287
+ <i class="bi bi-cloud-upload"></i>
1288
+ <h5>Drag & Drop or Click to Upload</h5>
1289
+ <p class="text-muted">CSV, Excel (L2, TargetSmart, VAN export)</p>
1290
+ <input type="file" class="d-none" id="voter-file" accept=".csv,.xlsx">
1291
+ <button class="btn btn-primary mt-2" onclick="document.getElementById('voter-file').click()">
1292
+ Select File
1293
+ </button>
1294
+ </div>
1295
+ </div>
1296
+ </div>
1297
+ </div>
1298
+ </div>
1299
+
1300
+ <div class="row">
1301
+ <div class="col-lg-6">
1302
+ <div class="card">
1303
+ <div class="card-header">
1304
+ <i class="bi bi-bar-chart"></i> Upload Demographics
1305
+ </div>
1306
+ <div class="card-body">
1307
+ <div class="file-upload">
1308
+ <i class="bi bi-cloud-upload"></i>
1309
+ <h5>Drag & Drop or Click to Upload</h5>
1310
+ <p class="text-muted">CSV, Excel (Census ACS tables)</p>
1311
+ <input type="file" class="d-none" id="demo-file" accept=".csv,.xlsx">
1312
+ <button class="btn btn-primary mt-2" onclick="document.getElementById('demo-file').click()">
1313
+ Select File
1314
+ </button>
1315
+ </div>
1316
+ </div>
1317
+ </div>
1318
+ </div>
1319
+
1320
+ <div class="col-lg-6">
1321
+ <div class="card">
1322
+ <div class="card-header">
1323
+ <i class="bi bi-map"></i> Upload Precinct Boundaries
1324
+ </div>
1325
+ <div class="card-body">
1326
+ <div class="file-upload">
1327
+ <i class="bi bi-cloud-upload"></i>
1328
+ <h5>Drag & Drop or Click to Upload</h5>
1329
+ <p class="text-muted">GeoJSON, Shapefile (VEST, TIGER/Line)</p>
1330
+ <input type="file" class="d-none" id="geo-file" accept=".geojson,.json,.zip">
1331
+ <button class="btn btn-primary mt-2" onclick="document.getElementById('geo-file').click()">
1332
+ Select File
1333
+ </button>
1334
+ </div>
1335
+ </div>
1336
+ </div>
1337
+ </div>
1338
+ </div>
1339
+
1340
+ <div class="card">
1341
+ <div class="card-header">
1342
+ <i class="bi bi-database"></i> Loaded Datasets
1343
+ </div>
1344
+ <div class="card-body">
1345
+ <div class="alert alert-success">
1346
+ <i class="bi bi-check-circle"></i> <strong>Sample District Data</strong> - 10 precincts, 2020-2024 results
1347
+ </div>
1348
+ <div class="alert alert-secondary">
1349
+ <i class="bi bi-x-circle"></i> No voter file loaded
1350
+ </div>
1351
+ <div class="alert alert-secondary">
1352
+ <i class="bi bi-x-circle"></i> No demographic data loaded
1353
+ </div>
1354
+ </div>
1355
+ </div>
1356
+ </div>
1357
+
1358
+ <!-- Export Section -->
1359
+ <div id="export" class="content-section">
1360
+ <h2>Export & Reports</h2>
1361
+ <p class="text-muted mb-4">Download analysis results, visualizations, and campaign reports</p>
1362
+
1363
+ <div class="card">
1364
+ <div class="card-header">
1365
+ <i class="bi bi-file-earmark-text"></i> Available Exports
1366
+ </div>
1367
+ <div class="card-body">
1368
+ <div class="list-group">
1369
+ <div class="list-group-item d-flex justify-content-between align-items-center">
1370
+ <div>
1371
+ <h6 class="mb-1">Precinct-Level Analysis (CSV)</h6>
1372
+ <small class="text-muted">All computed metrics, PVI, swing, turnout, strategic values</small>
1373
+ </div>
1374
+ <button class="btn btn-sm btn-primary">
1375
+ <i class="bi bi-download"></i> Download
1376
+ </button>
1377
+ </div>
1378
+
1379
+ <div class="list-group-item d-flex justify-content-between align-items-center">
1380
+ <div>
1381
+ <h6 class="mb-1">Voter Targeting Matrix (Excel)</h6>
1382
+ <small class="text-muted">Segment-by-segment breakdown with strategies</small>
1383
+ </div>
1384
+ <button class="btn btn-sm btn-primary">
1385
+ <i class="bi bi-download"></i> Download
1386
+ </button>
1387
+ </div>
1388
+
1389
+ <div class="list-group-item d-flex justify-content-between align-items-center">
1390
+ <div>
1391
+ <h6 class="mb-1">Monte Carlo Results (JSON)</h6>
1392
+ <small class="text-muted">Full simulation output with margin distribution</small>
1393
+ </div>
1394
+ <button class="btn btn-sm btn-primary">
1395
+ <i class="bi bi-download"></i> Download
1396
+ </button>
1397
+ </div>
1398
+
1399
+ <div class="list-group-item d-flex justify-content-between align-items-center">
1400
+ <div>
1401
+ <h6 class="mb-1">Interactive Maps (HTML)</h6>
1402
+ <small class="text-muted">Standalone HTML files with all visualizations</small>
1403
+ </div>
1404
+ <button class="btn btn-sm btn-primary">
1405
+ <i class="bi bi-download"></i> Download
1406
+ </button>
1407
+ </div>
1408
+
1409
+ <div class="list-group-item d-flex justify-content-between align-items-center">
1410
+ <div>
1411
+ <h6 class="mb-1">Campaign Strategy Report (PDF)</h6>
1412
+ <small class="text-muted">Executive summary with recommendations</small>
1413
+ </div>
1414
+ <button class="btn btn-sm btn-success">
1415
+ <i class="bi bi-file-pdf"></i> Generate PDF
1416
+ </button>
1417
+ </div>
1418
+
1419
+ <div class="list-group-item d-flex justify-content-between align-items-center">
1420
+ <div>
1421
+ <h6 class="mb-1">GOTV Turf Packets (Printable)</h6>
1422
+ <small class="text-muted">Walk lists by precinct with maps</small>
1423
+ </div>
1424
+ <button class="btn btn-sm btn-success">
1425
+ <i class="bi bi-printer"></i> Generate
1426
+ </button>
1427
+ </div>
1428
+ </div>
1429
+ </div>
1430
+ </div>
1431
+ </div>
1432
+
1433
+ </div>
1434
+
1435
+ <!-- Bootstrap JS -->
1436
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
1437
+
1438
+ <script>
1439
+ // Navigation
1440
+ document.querySelectorAll('.nav-item').forEach(item => {
1441
+ item.addEventListener('click', function() {
1442
+ // Update nav active state
1443
+ document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
1444
+ this.classList.add('active');
1445
+
1446
+ // Show corresponding section
1447
+ const sectionId = this.dataset.section;
1448
+ document.querySelectorAll('.content-section').forEach(section => {
1449
+ section.classList.remove('active');
1450
+ });
1451
+ document.getElementById(sectionId).classList.add('active');
1452
+ });
1453
+ });
1454
+
1455
+ // Initialize Dashboard with Sample Data
1456
+ function initializeDashboard() {
1457
+ // Update metrics
1458
+ document.getElementById('win-prob').textContent = '58.3%';
1459
+ document.getElementById('expected-margin').textContent = 'D+2.4';
1460
+ document.getElementById('high-value-precincts').textContent = '8';
1461
+ document.getElementById('contact-target').textContent = '24.5K';
1462
+
1463
+ // Sample scatter plot
1464
+ const sampleData = [
1465
+ {x: -5, y: 3, name: 'P01', size: 200, color: 'Red Lean'},
1466
+ {x: 2, y: 7, name: 'P02', size: 150, color: 'Swing'},
1467
+ {x: 8, y: 2, name: 'P03', size: 180, color: 'Blue Base'},
1468
+ {x: -2, y: 5, name: 'P04', size: 220, color: 'Swing'},
1469
+ {x: 12, y: 4, name: 'P05', size: 160, color: 'Blue Base'},
1470
+ ];
1471
+
1472
+ const trace = {
1473
+ x: sampleData.map(d => d.x),
1474
+ y: sampleData.map(d => d.y),
1475
+ text: sampleData.map(d => d.name),
1476
+ mode: 'markers',
1477
+ marker: {
1478
+ size: sampleData.map(d => d.size / 10),
1479
+ color: sampleData.map(d => d.color === 'Blue Base' ? '#1e40af' : d.color === 'Red Lean' ? '#dc2626' : '#9333ea')
1480
+ },
1481
+ type: 'scatter'
1482
+ };
1483
+
1484
+ Plotly.newPlot('district-map', [trace], {
1485
+ title: 'Precinct Positioning',
1486
+ xaxis: {title: 'PVI (Partisan Lean)'},
1487
+ yaxis: {title: 'Swing Score'},
1488
+ hovermode: 'closest'
1489
+ }, {responsive: true});
1490
+
1491
+ // Demographic pie
1492
+ Plotly.newPlot('demo-pie', [{
1493
+ values: [35, 28, 22, 15],
1494
+ labels: ['White Non-Hispanic', 'Hispanic/Latino', 'Black/AA', 'Asian/Other'],
1495
+ type: 'pie',
1496
+ marker: {
1497
+ colors: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6']
1498
+ }
1499
+ }], {
1500
+ title: 'Racial/Ethnic Composition'
1501
+ }, {responsive: true});
1502
+
1503
+ // Win probability timeline
1504
+ const dates = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct'];
1505
+ const probs = [45, 48, 50, 52, 54, 56, 57, 58, 58, 58.3];
1506
+
1507
+ Plotly.newPlot('prob-timeline', [{
1508
+ x: dates,
1509
+ y: probs,
1510
+ type: 'scatter',
1511
+ mode: 'lines+markers',
1512
+ line: {color: '#1e40af', width: 3},
1513
+ fill: 'tozeroy'
1514
+ }], {
1515
+ yaxis: {title: 'Win Probability (%)', range: [0, 100]},
1516
+ xaxis: {title: '2024'}
1517
+ }, {responsive: true});
1518
+ }
1519
+
1520
+ // Placeholder functions (would connect to backend/data processing)
1521
+ function generateDemoSegmentation() {
1522
+ alert('Generating demographic segmentation...
1523
+
1524
+ This would analyze uploaded data and compute segment-level metrics.');
1525
+ }
1526
+
1527
+ function analyzePrecincts() {
1528
+ alert('Running precinct-level analysis...
1529
+
1530
+ This would compute PVI, swing, elasticity, and clustering for all precincts.');
1531
+ }
1532
+
1533
+ function runMonteCarloSimulation() {
1534
+ document.getElementById('mc-spinner').classList.add('active');
1535
+ setTimeout(() => {
1536
+ document.getElementById('mc-spinner').classList.remove('active');
1537
+ document.getElementById('mc-win-prob').textContent = '58.3%';
1538
+ document.getElementById('mc-margin').textContent = 'D+2.4';
1539
+ document.getElementById('mc-recount').textContent = '3.2%';
1540
+
1541
+ // Generate histogram
1542
+ const margins = Array.from({length: 1000}, () =>
1543
+ Math.random() * 10 - 3
1544
+ );
1545
+
1546
+ Plotly.newPlot('mc-histogram', [{
1547
+ x: margins,
1548
+ type: 'histogram',
1549
+ nbinsx: 50,
1550
+ marker: {color: '#1e40af'}
1551
+ }], {
1552
+ title: '10,000 Simulated Election Outcomes',
1553
+ xaxis: {title: 'D Margin (%)'},
1554
+ yaxis: {title: 'Frequency'}
1555
+ }, {responsive: true});
1556
+ }, 2000);
1557
+ }
1558
+
1559
+ function resetSimulation() {
1560
+ document.getElementById('mc-win-prob').textContent = '--';
1561
+ document.getElementById('mc-margin').textContent = '--';
1562
+ document.getElementById('mc-recount').textContent = '--';
1563
+ Plotly.purge('mc-histogram');
1564
+ }
1565
+
1566
+ function runScenario() {
1567
+ alert('Calculating scenario...
1568
+
1569
+ This would re-run the model with adjusted demographic/turnout parameters.');
1570
+ }
1571
+
1572
+ function buildUniverse() {
1573
+ document.getElementById('universe-size').textContent = '24,518';
1574
+ document.getElementById('contact-rate').textContent = '65%';
1575
+ document.getElementById('vote-yield').textContent = '+1,842';
1576
+ alert('Universe generated!
1577
+
1578
+ 24,518 voters identified across persuasion and mobilization targets.');
1579
+ }
1580
+
1581
+ function optimizeTurf() {
1582
+ alert('Optimizing turf allocation...
1583
+
1584
+ This would rank precincts and allocate volunteer resources to maximize vote yield.');
1585
+ }
1586
+
1587
+ // Initialize on load
1588
+ window.addEventListener('load', initializeDashboard);
1589
+ </script>
1590
+ </body>
1591
+ </html>
static/politicoai_gui.html ADDED
File without changes