Spaces:
No application file
No application file
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>PoliticoAI - Campaign Strategy Platform</title> | |
| <!-- Bootstrap CSS --> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> | |
| <!-- Bootstrap Icons --> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet"> | |
| <!-- Plotly --> | |
| <script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script> | |
| <style> | |
| :root { | |
| --primary-blue: #1e40af; | |
| --primary-red: #dc2626; | |
| --sidebar-width: 260px; | |
| --header-height: 60px; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; | |
| background: #f8fafc; | |
| overflow-x: hidden; | |
| } | |
| /* Header */ | |
| .app-header { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: var(--header-height); | |
| background: linear-gradient(135deg, var(--primary-blue) 0%, #3b82f6 100%); | |
| color: white; | |
| display: flex; | |
| align-items: center; | |
| padding: 0 24px; | |
| box-shadow: 0 2px 8px rgba(0,0,0,0.1); | |
| z-index: 1000; | |
| } | |
| .app-header h1 { | |
| font-size: 24px; | |
| font-weight: 700; | |
| margin: 0; | |
| } | |
| .app-header .badge { | |
| margin-left: 12px; | |
| font-size: 11px; | |
| font-weight: 600; | |
| } | |
| /* Sidebar */ | |
| .sidebar { | |
| position: fixed; | |
| top: var(--header-height); | |
| left: 0; | |
| width: var(--sidebar-width); | |
| height: calc(100vh - var(--header-height)); | |
| background: white; | |
| border-right: 1px solid #e2e8f0; | |
| overflow-y: auto; | |
| z-index: 999; | |
| } | |
| .nav-section { | |
| padding: 24px 0; | |
| border-bottom: 1px solid #e2e8f0; | |
| } | |
| .nav-section-title { | |
| padding: 0 20px 8px; | |
| font-size: 11px; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| color: #64748b; | |
| letter-spacing: 0.5px; | |
| } | |
| .nav-item { | |
| padding: 10px 20px; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| color: #475569; | |
| transition: all 0.2s; | |
| } | |
| .nav-item:hover { | |
| background: #f1f5f9; | |
| color: var(--primary-blue); | |
| } | |
| .nav-item.active { | |
| background: #eff6ff; | |
| color: var(--primary-blue); | |
| border-left: 3px solid var(--primary-blue); | |
| } | |
| .nav-item i { | |
| width: 20px; | |
| margin-right: 12px; | |
| font-size: 18px; | |
| } | |
| /* Main Content */ | |
| .main-content { | |
| margin-left: var(--sidebar-width); | |
| margin-top: var(--header-height); | |
| padding: 32px; | |
| min-height: calc(100vh - var(--header-height)); | |
| } | |
| .content-section { | |
| display: none; | |
| } | |
| .content-section.active { | |
| display: block; | |
| } | |
| /* Cards */ | |
| .card { | |
| border: none; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| border-radius: 8px; | |
| margin-bottom: 24px; | |
| } | |
| .card-header { | |
| background: white; | |
| border-bottom: 1px solid #e2e8f0; | |
| padding: 20px 24px; | |
| font-weight: 600; | |
| color: #1e293b; | |
| } | |
| .card-body { | |
| padding: 24px; | |
| } | |
| /* Metrics Grid */ | |
| .metrics-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 32px; | |
| } | |
| .metric-card { | |
| background: white; | |
| padding: 24px; | |
| border-radius: 8px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| border-left: 4px solid; | |
| } | |
| .metric-card.blue { | |
| border-left-color: var(--primary-blue); | |
| } | |
| .metric-card.red { | |
| border-left-color: var(--primary-red); | |
| } | |
| .metric-card.purple { | |
| border-left-color: #9333ea; | |
| } | |
| .metric-card.green { | |
| border-left-color: #16a34a; | |
| } | |
| .metric-label { | |
| font-size: 13px; | |
| font-weight: 600; | |
| color: #64748b; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .metric-value { | |
| font-size: 32px; | |
| font-weight: 700; | |
| color: #1e293b; | |
| margin-top: 8px; | |
| } | |
| .metric-change { | |
| font-size: 13px; | |
| margin-top: 8px; | |
| } | |
| .metric-change.positive { | |
| color: #16a34a; | |
| } | |
| .metric-change.negative { | |
| color: #dc2626; | |
| } | |
| /* Forms */ | |
| .form-label { | |
| font-weight: 600; | |
| font-size: 14px; | |
| color: #334155; | |
| margin-bottom: 8px; | |
| } | |
| .form-control, .form-select { | |
| border: 1px solid #cbd5e1; | |
| border-radius: 6px; | |
| padding: 10px 14px; | |
| } | |
| .form-control:focus, .form-select:focus { | |
| border-color: var(--primary-blue); | |
| box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1); | |
| } | |
| /* Buttons */ | |
| .btn { | |
| padding: 10px 20px; | |
| border-radius: 6px; | |
| font-weight: 600; | |
| transition: all 0.2s; | |
| } | |
| .btn-primary { | |
| background: var(--primary-blue); | |
| border: none; | |
| } | |
| .btn-primary:hover { | |
| background: #1e3a8a; | |
| transform: translateY(-1px); | |
| } | |
| .btn-success { | |
| background: #16a34a; | |
| border: none; | |
| } | |
| .btn-danger { | |
| background: var(--primary-red); | |
| border: none; | |
| } | |
| /* Tables */ | |
| .table { | |
| font-size: 14px; | |
| } | |
| .table thead th { | |
| background: #f8fafc; | |
| color: #475569; | |
| font-weight: 600; | |
| border-bottom: 2px solid #e2e8f0; | |
| text-transform: uppercase; | |
| font-size: 12px; | |
| letter-spacing: 0.5px; | |
| } | |
| .table tbody tr:hover { | |
| background: #f8fafc; | |
| } | |
| /* Visualization Container */ | |
| .viz-container { | |
| background: white; | |
| border-radius: 8px; | |
| padding: 24px; | |
| margin-top: 24px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| } | |
| /* Alerts */ | |
| .alert { | |
| border: none; | |
| border-radius: 6px; | |
| border-left: 4px solid; | |
| } | |
| .alert-info { | |
| border-left-color: #3b82f6; | |
| background: #eff6ff; | |
| color: #1e40af; | |
| } | |
| .alert-success { | |
| border-left-color: #16a34a; | |
| background: #f0fdf4; | |
| color: #15803d; | |
| } | |
| .alert-warning { | |
| border-left-color: #f59e0b; | |
| background: #fffbeb; | |
| color: #b45309; | |
| } | |
| /* Loading Spinner */ | |
| .spinner { | |
| display: none; | |
| text-align: center; | |
| padding: 40px; | |
| } | |
| .spinner.active { | |
| display: block; | |
| } | |
| /* Range Slider Custom Style */ | |
| input[type="range"] { | |
| width: 100%; | |
| height: 6px; | |
| border-radius: 3px; | |
| background: #e2e8f0; | |
| outline: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: var(--primary-blue); | |
| cursor: pointer; | |
| } | |
| input[type="range"]::-moz-range-thumb { | |
| width: 20px; | |
| height: 20px; | |
| border-radius: 50%; | |
| background: var(--primary-blue); | |
| cursor: pointer; | |
| } | |
| .slider-value { | |
| display: inline-block; | |
| min-width: 50px; | |
| text-align: center; | |
| font-weight: 600; | |
| color: var(--primary-blue); | |
| } | |
| /* File Upload */ | |
| .file-upload { | |
| border: 2px dashed #cbd5e1; | |
| border-radius: 8px; | |
| padding: 40px; | |
| text-align: center; | |
| background: #f8fafc; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .file-upload:hover { | |
| border-color: var(--primary-blue); | |
| background: #eff6ff; | |
| } | |
| .file-upload i { | |
| font-size: 48px; | |
| color: #94a3b8; | |
| margin-bottom: 16px; | |
| } | |
| /* Progress Bar */ | |
| .progress { | |
| height: 8px; | |
| border-radius: 4px; | |
| background: #e2e8f0; | |
| } | |
| .progress-bar { | |
| background: var(--primary-blue); | |
| border-radius: 4px; | |
| } | |
| /* Tabs */ | |
| .nav-tabs { | |
| border-bottom: 2px solid #e2e8f0; | |
| } | |
| .nav-tabs .nav-link { | |
| border: none; | |
| color: #64748b; | |
| font-weight: 600; | |
| padding: 12px 24px; | |
| } | |
| .nav-tabs .nav-link.active { | |
| color: var(--primary-blue); | |
| border-bottom: 2px solid var(--primary-blue); | |
| margin-bottom: -2px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <div class="app-header"> | |
| <h1><i class="bi bi-graph-up-arrow"></i> PoliticoAI</h1> | |
| <span class="badge bg-light text-primary">Campaign Strategy Platform</span> | |
| </div> | |
| <!-- Sidebar Navigation --> | |
| <div class="sidebar"> | |
| <div class="nav-section"> | |
| <div class="nav-section-title">Analytics</div> | |
| <div class="nav-item active" data-section="dashboard"> | |
| <i class="bi bi-speedometer2"></i> | |
| <span>Dashboard</span> | |
| </div> | |
| <div class="nav-item" data-section="demographics"> | |
| <i class="bi bi-people"></i> | |
| <span>Demographics</span> | |
| </div> | |
| <div class="nav-item" data-section="precincts"> | |
| <i class="bi bi-map"></i> | |
| <span>Precinct Analysis</span> | |
| </div> | |
| </div> | |
| <div class="nav-section"> | |
| <div class="nav-section-title">Modeling</div> | |
| <div class="nav-item" data-section="simulation"> | |
| <i class="bi bi-cpu"></i> | |
| <span>Monte Carlo</span> | |
| </div> | |
| <div class="nav-item" data-section="scenarios"> | |
| <i class="bi bi-sliders"></i> | |
| <span>Scenarios</span> | |
| </div> | |
| <div class="nav-item" data-section="forecasting"> | |
| <i class="bi bi-graph-up"></i> | |
| <span>Forecasting</span> | |
| </div> | |
| </div> | |
| <div class="nav-section"> | |
| <div class="nav-section-title">Strategy</div> | |
| <div class="nav-item" data-section="targeting"> | |
| <i class="bi bi-bullseye"></i> | |
| <span>Voter Targeting</span> | |
| </div> | |
| <div class="nav-item" data-section="gotv"> | |
| <i class="bi bi-megaphone"></i> | |
| <span>GOTV Planning</span> | |
| </div> | |
| <div class="nav-item" data-section="messaging"> | |
| <i class="bi bi-chat-square-text"></i> | |
| <span>Messaging</span> | |
| </div> | |
| </div> | |
| <div class="nav-section"> | |
| <div class="nav-section-title">Data</div> | |
| <div class="nav-item" data-section="upload"> | |
| <i class="bi bi-upload"></i> | |
| <span>Data Upload</span> | |
| </div> | |
| <div class="nav-item" data-section="export"> | |
| <i class="bi bi-download"></i> | |
| <span>Export</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <div class="main-content"> | |
| <!-- Dashboard Section --> | |
| <div id="dashboard" class="content-section active"> | |
| <h2>Campaign Dashboard</h2> | |
| <p class="text-muted mb-4">Real-time overview of district performance and strategic metrics</p> | |
| <div class="metrics-grid"> | |
| <div class="metric-card blue"> | |
| <div class="metric-label">Current Win Probability</div> | |
| <div class="metric-value" id="win-prob">--</div> | |
| <div class="metric-change positive" id="win-prob-change"> | |
| <i class="bi bi-arrow-up"></i> +5.2% vs. baseline | |
| </div> | |
| </div> | |
| <div class="metric-card purple"> | |
| <div class="metric-label">Expected Margin</div> | |
| <div class="metric-value" id="expected-margin">--</div> | |
| <div class="metric-change" id="margin-change"> | |
| ±2.1% (95% CI) | |
| </div> | |
| </div> | |
| <div class="metric-card green"> | |
| <div class="metric-label">High-Value Precincts</div> | |
| <div class="metric-value" id="high-value-precincts">--</div> | |
| <div class="metric-change"> | |
| <i class="bi bi-geo-alt"></i> Top quartile by net value | |
| </div> | |
| </div> | |
| <div class="metric-card red"> | |
| <div class="metric-label">Voter Contact Target</div> | |
| <div class="metric-value" id="contact-target">--</div> | |
| <div class="metric-change"> | |
| <i class="bi bi-person-check"></i> Doors + phones | |
| </div> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div class="col-lg-8"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-map-fill"></i> District Map - Partisan Lean (PVI) | |
| </div> | |
| <div class="card-body"> | |
| <div id="district-map" style="height: 400px;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-lg-4"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-pie-chart-fill"></i> Demographic Breakdown | |
| </div> | |
| <div class="card-body"> | |
| <div id="demo-pie" style="height: 400px;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-graph-up"></i> Win Probability Over Time | |
| </div> | |
| <div class="card-body"> | |
| <div id="prob-timeline" style="height: 300px;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Demographics Section --> | |
| <div id="demographics" class="content-section"> | |
| <h2>Demographic Segmentation</h2> | |
| <p class="text-muted mb-4">Analyze voter universe by age, education, race, income, and geography</p> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-sliders"></i> Segmentation Controls | |
| </div> | |
| <div class="card-body"> | |
| <div class="row"> | |
| <div class="col-md-4"> | |
| <label class="form-label">Primary Dimension</label> | |
| <select class="form-select" id="demo-primary"> | |
| <option value="age">Age Cohort</option> | |
| <option value="education">Education Level</option> | |
| <option value="race">Race/Ethnicity</option> | |
| <option value="income">Income Quintile</option> | |
| <option value="geography">Geography (Urban/Suburban/Rural)</option> | |
| </select> | |
| </div> | |
| <div class="col-md-4"> | |
| <label class="form-label">Secondary Dimension</label> | |
| <select class="form-select" id="demo-secondary"> | |
| <option value="none">None</option> | |
| <option value="age">Age Cohort</option> | |
| <option value="education">Education Level</option> | |
| <option value="race">Race/Ethnicity</option> | |
| <option value="income">Income Quintile</option> | |
| </select> | |
| </div> | |
| <div class="col-md-4"> | |
| <label class="form-label">Metric to Display</label> | |
| <select class="form-select" id="demo-metric"> | |
| <option value="size">Segment Size</option> | |
| <option value="turnout">Turnout Rate</option> | |
| <option value="lean">Partisan Lean</option> | |
| <option value="persuadability">Persuadability</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="mt-3"> | |
| <button class="btn btn-primary" onclick="generateDemoSegmentation()"> | |
| <i class="bi bi-play-fill"></i> Generate Segmentation | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="viz-container"> | |
| <div id="demo-treemap" style="height: 500px;"></div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-table"></i> Targeting Matrix | |
| </div> | |
| <div class="card-body"> | |
| <div class="table-responsive"> | |
| <table class="table table-hover" id="targeting-matrix"> | |
| <thead> | |
| <tr> | |
| <th>Segment</th> | |
| <th>Size</th> | |
| <th>Turnout Rate</th> | |
| <th>Partisan Lean</th> | |
| <th>Persuadability</th> | |
| <th>Strategy</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td colspan="6" class="text-center text-muted"> | |
| Run segmentation to populate table | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Precinct Analysis Section --> | |
| <div id="precincts" class="content-section"> | |
| <h2>Precinct-Level Analysis</h2> | |
| <p class="text-muted mb-4">Deep dive into precinct metrics, clustering, and strategic prioritization</p> | |
| <div class="alert alert-info"> | |
| <i class="bi bi-info-circle"></i> | |
| <strong>Analysis Level:</strong> This module computes PVI, swing scores, elasticity, turnout metrics, and strategic value for every precinct. | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-gear"></i> Analysis Parameters | |
| </div> | |
| <div class="card-body"> | |
| <div class="row"> | |
| <div class="col-md-4"> | |
| <label class="form-label">Clustering Method</label> | |
| <select class="form-select" id="cluster-method"> | |
| <option value="kmeans">K-Means</option> | |
| <option value="hierarchical">Hierarchical (Ward)</option> | |
| </select> | |
| </div> | |
| <div class="col-md-4"> | |
| <label class="form-label">Number of Clusters</label> | |
| <select class="form-select" id="num-clusters"> | |
| <option value="4">4 Tiers</option> | |
| <option value="5">5 Tiers</option> | |
| <option value="6">6 Tiers</option> | |
| <option value="8">8 Tiers</option> | |
| </select> | |
| </div> | |
| <div class="col-md-4"> | |
| <label class="form-label">Priority Metric</label> | |
| <select class="form-select" id="priority-metric"> | |
| <option value="net">Net Mobilization Score</option> | |
| <option value="persuasion">Persuasion Value</option> | |
| <option value="mobilization">Mobilization Value</option> | |
| <option value="competitiveness">Competitiveness</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="mt-3"> | |
| <button class="btn btn-primary" onclick="analyzePrecincts()"> | |
| <i class="bi bi-play-fill"></i> Run Precinct Analysis | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div class="col-lg-6"> | |
| <div class="viz-container"> | |
| <h5>PVI vs. Swing Score (by Cluster)</h5> | |
| <div id="precinct-scatter" style="height: 400px;"></div> | |
| </div> | |
| </div> | |
| <div class="col-lg-6"> | |
| <div class="viz-container"> | |
| <h5>Precinct Priority Ranking</h5> | |
| <div id="precinct-ranking" style="height: 400px;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-list-ol"></i> Top 20 Precincts by Strategic Value | |
| </div> | |
| <div class="card-body"> | |
| <div class="table-responsive"> | |
| <table class="table table-sm" id="precinct-table"> | |
| <thead> | |
| <tr> | |
| <th>Rank</th> | |
| <th>Precinct</th> | |
| <th>PVI</th> | |
| <th>Swing</th> | |
| <th>TO Delta</th> | |
| <th>Net Value</th> | |
| <th>Tier</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td colspan="7" class="text-center text-muted"> | |
| Run analysis to populate table | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Monte Carlo Simulation Section --> | |
| <div id="simulation" class="content-section"> | |
| <h2>Monte Carlo Simulation</h2> | |
| <p class="text-muted mb-4">Run thousands of election simulations to estimate win probability and margin distribution</p> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-sliders"></i> Simulation Parameters | |
| </div> | |
| <div class="card-body"> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <label class="form-label">Number of Iterations</label> | |
| <select class="form-select" id="mc-iterations"> | |
| <option value="1000">1,000 (Quick)</option> | |
| <option value="5000">5,000 (Standard)</option> | |
| <option value="10000" selected>10,000 (Recommended)</option> | |
| <option value="50000">50,000 (Tight Race)</option> | |
| </select> | |
| </div> | |
| <div class="col-md-6"> | |
| <label class="form-label">Baseline Environment</label> | |
| <select class="form-select" id="mc-environment"> | |
| <option value="neutral">Neutral / Even</option> | |
| <option value="d3">D+3 Environment</option> | |
| <option value="r3">R+3 Environment</option> | |
| <option value="d5">D+5 Wave</option> | |
| <option value="r5">R+5 Wave</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="row mt-3"> | |
| <div class="col-md-6"> | |
| <label class="form-label"> | |
| Polling Error (σ): <span class="slider-value" id="polling-error-val">3.5</span>% | |
| </label> | |
| <input type="range" id="polling-error" min="1" max="6" step="0.5" value="3.5" | |
| oninput="document.getElementById('polling-error-val').textContent = this.value"> | |
| </div> | |
| <div class="col-md-6"> | |
| <label class="form-label"> | |
| Turnout Variability: <span class="slider-value" id="turnout-var-val">5</span>% | |
| </label> | |
| <input type="range" id="turnout-var" min="2" max="10" step="1" value="5" | |
| oninput="document.getElementById('turnout-var-val').textContent = this.value"> | |
| </div> | |
| </div> | |
| <div class="mt-4"> | |
| <button class="btn btn-primary btn-lg" onclick="runMonteCarloSimulation()"> | |
| <i class="bi bi-play-circle-fill"></i> Run Simulation | |
| </button> | |
| <button class="btn btn-secondary" onclick="resetSimulation()"> | |
| <i class="bi bi-arrow-clockwise"></i> Reset | |
| </button> | |
| </div> | |
| <div id="mc-spinner" class="spinner"> | |
| <div class="spinner-border text-primary" role="status"></div> | |
| <p class="mt-3">Running simulation...</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div class="col-lg-4"> | |
| <div class="metric-card blue"> | |
| <div class="metric-label">Win Probability</div> | |
| <div class="metric-value" id="mc-win-prob">--</div> | |
| </div> | |
| </div> | |
| <div class="col-lg-4"> | |
| <div class="metric-card purple"> | |
| <div class="metric-label">Expected Margin</div> | |
| <div class="metric-value" id="mc-margin">--</div> | |
| </div> | |
| </div> | |
| <div class="col-lg-4"> | |
| <div class="metric-card green"> | |
| <div class="metric-label">Recount Probability</div> | |
| <div class="metric-value" id="mc-recount">--</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="viz-container"> | |
| <h5>Margin Distribution (10,000 Simulations)</h5> | |
| <div id="mc-histogram" style="height: 400px;"></div> | |
| </div> | |
| </div> | |
| <!-- Scenarios Section --> | |
| <div id="scenarios" class="content-section"> | |
| <h2>Scenario Builder</h2> | |
| <p class="text-muted mb-4">Model "what if" scenarios with demographic shifts, turnout changes, and third-party effects</p> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-plus-circle"></i> Build Scenario | |
| </div> | |
| <div class="card-body"> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <label class="form-label">Scenario Name</label> | |
| <input type="text" class="form-control" id="scenario-name" placeholder="e.g., High Youth Turnout"> | |
| </div> | |
| <div class="col-md-6"> | |
| <label class="form-label">Compare Against</label> | |
| <select class="form-select" id="scenario-baseline"> | |
| <option value="2024">2024 Baseline</option> | |
| <option value="2022">2022 Midterm</option> | |
| <option value="2020">2020 Presidential</option> | |
| </select> | |
| </div> | |
| </div> | |
| <hr class="my-4"> | |
| <h6 class="mb-3">Turnout Adjustments (by demographic)</h6> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <label class="form-label"> | |
| 18-29 Turnout: <span class="slider-value" id="turnout-18-29-val">0</span>% | |
| </label> | |
| <input type="range" id="turnout-18-29" min="-30" max="30" step="5" value="0" | |
| oninput="document.getElementById('turnout-18-29-val').textContent = (this.value > 0 ? '+' : '') + this.value"> | |
| </div> | |
| <div class="col-md-6"> | |
| <label class="form-label"> | |
| College+ Turnout: <span class="slider-value" id="turnout-college-val">0</span>% | |
| </label> | |
| <input type="range" id="turnout-college" min="-30" max="30" step="5" value="0" | |
| oninput="document.getElementById('turnout-college-val').textContent = (this.value > 0 ? '+' : '') + this.value"> | |
| </div> | |
| </div> | |
| <h6 class="mb-3 mt-4">Partisan Shifts (by demographic)</h6> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <label class="form-label"> | |
| Hispanic D Margin: <span class="slider-value" id="hispanic-shift-val">0</span> pts | |
| </label> | |
| <input type="range" id="hispanic-shift" min="-15" max="15" step="1" value="0" | |
| oninput="document.getElementById('hispanic-shift-val').textContent = (this.value > 0 ? '+' : '') + this.value"> | |
| </div> | |
| <div class="col-md-6"> | |
| <label class="form-label"> | |
| Suburban Women D Margin: <span class="slider-value" id="subwomen-shift-val">0</span> pts | |
| </label> | |
| <input type="range" id="subwomen-shift" min="-15" max="15" step="1" value="0" | |
| oninput="document.getElementById('subwomen-shift-val').textContent = (this.value > 0 ? '+' : '') + this.value"> | |
| </div> | |
| </div> | |
| <div class="mt-4"> | |
| <button class="btn btn-success" onclick="runScenario()"> | |
| <i class="bi bi-calculator"></i> Calculate Scenario | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-bar-chart-line"></i> Scenario Comparison | |
| </div> | |
| <div class="card-body"> | |
| <div id="scenario-comparison" style="height: 400px;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Voter Targeting Section --> | |
| <div id="targeting" class="content-section"> | |
| <h2>Voter Targeting</h2> | |
| <p class="text-muted mb-4">Build persuasion and mobilization universes with data-driven segment prioritization</p> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-funnel"></i> Build Universe | |
| </div> | |
| <div class="card-body"> | |
| <div class="row"> | |
| <div class="col-md-4"> | |
| <label class="form-label">Universe Type</label> | |
| <select class="form-select" id="universe-type"> | |
| <option value="persuasion">Persuasion</option> | |
| <option value="mobilization">Mobilization</option> | |
| <option value="combined">Combined</option> | |
| </select> | |
| </div> | |
| <div class="col-md-4"> | |
| <label class="form-label">Competitiveness Threshold</label> | |
| <select class="form-select" id="comp-threshold"> | |
| <option value="0.7">High (>70% competitive)</option> | |
| <option value="0.5" selected>Medium (>50%)</option> | |
| <option value="0.3">Low (>30%)</option> | |
| </select> | |
| </div> | |
| <div class="col-md-4"> | |
| <label class="form-label">Min Support Score</label> | |
| <select class="form-select" id="support-score"> | |
| <option value="3">Lean Support (3+)</option> | |
| <option value="4" selected>Likely Support (4+)</option> | |
| <option value="5">Strong Support (5)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="mt-3"> | |
| <button class="btn btn-primary" onclick="buildUniverse()"> | |
| <i class="bi bi-play-fill"></i> Generate Universe | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="metrics-grid"> | |
| <div class="metric-card blue"> | |
| <div class="metric-label">Universe Size</div> | |
| <div class="metric-value" id="universe-size">--</div> | |
| </div> | |
| <div class="metric-card purple"> | |
| <div class="metric-label">Contact Rate Goal</div> | |
| <div class="metric-value" id="contact-rate">--</div> | |
| </div> | |
| <div class="metric-card green"> | |
| <div class="metric-label">Estimated Vote Yield</div> | |
| <div class="metric-value" id="vote-yield">--</div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-people-fill"></i> Segment Performance | |
| </div> | |
| <div class="card-body"> | |
| <div id="segment-performance" style="height: 400px;"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- GOTV Section --> | |
| <div id="gotv" class="content-section"> | |
| <h2>GOTV Planning</h2> | |
| <p class="text-muted mb-4">Optimize canvass turf, phone banks, and volunteer deployment</p> | |
| <div class="alert alert-warning"> | |
| <i class="bi bi-exclamation-triangle"></i> | |
| <strong>GOTV Window:</strong> Most effective in final 10-14 days. Focus on identified supporters only. | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-calendar-check"></i> Resource Allocation | |
| </div> | |
| <div class="card-body"> | |
| <div class="row"> | |
| <div class="col-md-4"> | |
| <label class="form-label">Volunteer Hours Available</label> | |
| <input type="number" class="form-control" id="volunteer-hours" value="500"> | |
| </div> | |
| <div class="col-md-4"> | |
| <label class="form-label">Phone Bank Capacity</label> | |
| <input type="number" class="form-control" id="phone-capacity" value="10000"> | |
| </div> | |
| <div class="col-md-4"> | |
| <label class="form-label">Days Until Election</label> | |
| <input type="number" class="form-control" id="days-to-election" value="14"> | |
| </div> | |
| </div> | |
| <div class="row mt-3"> | |
| <div class="col-md-6"> | |
| <label class="form-label">Contact Mix</label> | |
| <select class="form-select" id="contact-mix"> | |
| <option value="balanced">Balanced (50/50 doors/phones)</option> | |
| <option value="door-heavy">Door-Heavy (70/30)</option> | |
| <option value="phone-heavy">Phone-Heavy (30/70)</option> | |
| </select> | |
| </div> | |
| <div class="col-md-6"> | |
| <label class="form-label">Priority Mode</label> | |
| <select class="form-select" id="priority-mode"> | |
| <option value="net">Net Value (Balanced)</option> | |
| <option value="persuasion">Persuasion Focus</option> | |
| <option value="mobilization">Turnout Focus</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="mt-4"> | |
| <button class="btn btn-success btn-lg" onclick="optimizeTurf()"> | |
| <i class="bi bi-geo-alt-fill"></i> Optimize Turf Cutting | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-map"></i> Recommended Precinct Allocation | |
| </div> | |
| <div class="card-body"> | |
| <div class="table-responsive"> | |
| <table class="table" id="turf-allocation"> | |
| <thead> | |
| <tr> | |
| <th>Priority</th> | |
| <th>Precinct</th> | |
| <th>Doors</th> | |
| <th>Phone Attempts</th> | |
| <th>Vol. Hours</th> | |
| <th>Est. Votes</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td colspan="6" class="text-center text-muted"> | |
| Run optimization to generate allocation | |
| </td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Messaging Section --> | |
| <div id="messaging" class="content-section"> | |
| <h2>Messaging Strategy</h2> | |
| <p class="text-muted mb-4">Issue salience mapping and message-to-market fit analysis</p> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-list-check"></i> Issue Priorities by Segment | |
| </div> | |
| <div class="card-body"> | |
| <div class="table-responsive"> | |
| <table class="table"> | |
| <thead> | |
| <tr> | |
| <th>Issue</th> | |
| <th>Young Urban</th> | |
| <th>Suburban Swing</th> | |
| <th>Rural Senior</th> | |
| <th>Overall</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr> | |
| <td><strong>Economy/Jobs</strong></td> | |
| <td><span class="badge bg-warning">Medium</span></td> | |
| <td><span class="badge bg-danger">Very High</span></td> | |
| <td><span class="badge bg-danger">Very High</span></td> | |
| <td><span class="badge bg-danger">Very High</span></td> | |
| </tr> | |
| <tr> | |
| <td><strong>Healthcare</strong></td> | |
| <td><span class="badge bg-success">High</span></td> | |
| <td><span class="badge bg-success">High</span></td> | |
| <td><span class="badge bg-danger">Very High</span></td> | |
| <td><span class="badge bg-success">High</span></td> | |
| </tr> | |
| <tr> | |
| <td><strong>Abortion/Reproductive Rights</strong></td> | |
| <td><span class="badge bg-danger">Very High</span></td> | |
| <td><span class="badge bg-success">High</span></td> | |
| <td><span class="badge bg-secondary">Low</span></td> | |
| <td><span class="badge bg-success">High</span></td> | |
| </tr> | |
| <tr> | |
| <td><strong>Immigration</strong></td> | |
| <td><span class="badge bg-secondary">Low</span></td> | |
| <td><span class="badge bg-warning">Medium</span></td> | |
| <td><span class="badge bg-danger">Very High</span></td> | |
| <td><span class="badge bg-success">High</span></td> | |
| </tr> | |
| <tr> | |
| <td><strong>Climate/Environment</strong></td> | |
| <td><span class="badge bg-danger">Very High</span></td> | |
| <td><span class="badge bg-warning">Medium</span></td> | |
| <td><span class="badge bg-secondary">Low</span></td> | |
| <td><span class="badge bg-warning">Medium</span></td> | |
| </tr> | |
| <tr> | |
| <td><strong>Education</strong></td> | |
| <td><span class="badge bg-warning">Medium</span></td> | |
| <td><span class="badge bg-danger">Very High</span></td> | |
| <td><span class="badge bg-warning">Medium</span></td> | |
| <td><span class="badge bg-success">High</span></td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="alert alert-info"> | |
| <i class="bi bi-lightbulb"></i> | |
| <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. | |
| </div> | |
| </div> | |
| <!-- Forecasting Section --> | |
| <div id="forecasting" class="content-section"> | |
| <h2>Election Forecasting</h2> | |
| <p class="text-muted mb-4">Bayesian model with polling integration and early vote tracking</p> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-graph-up"></i> Model Inputs | |
| </div> | |
| <div class="card-body"> | |
| <ul class="nav nav-tabs" role="tablist"> | |
| <li class="nav-item"> | |
| <a class="nav-link active" data-bs-toggle="tab" href="#fundamentals-tab">Fundamentals</a> | |
| </li> | |
| <li class="nav-item"> | |
| <a class="nav-link" data-bs-toggle="tab" href="#polling-tab">Polling</a> | |
| </li> | |
| <li class="nav-item"> | |
| <a class="nav-link" data-bs-toggle="tab" href="#early-vote-tab">Early Vote</a> | |
| </li> | |
| </ul> | |
| <div class="tab-content mt-3"> | |
| <div id="fundamentals-tab" class="tab-pane fade show active"> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <label class="form-label">Incumbent Party</label> | |
| <select class="form-select"> | |
| <option>Democrat</option> | |
| <option>Republican</option> | |
| <option>Open Seat</option> | |
| </select> | |
| </div> | |
| <div class="col-md-6"> | |
| <label class="form-label">Generic Ballot (D+/-)</label> | |
| <input type="number" class="form-control" value="0" step="0.5"> | |
| </div> | |
| </div> | |
| <div class="row mt-3"> | |
| <div class="col-md-6"> | |
| <label class="form-label">Presidential Approval</label> | |
| <input type="number" class="form-control" value="45" step="1"> | |
| </div> | |
| <div class="col-md-6"> | |
| <label class="form-label">Right Track/Wrong Track</label> | |
| <input type="number" class="form-control" value="35" step="1"> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="polling-tab" class="tab-pane fade"> | |
| <p class="text-muted">Add recent polls to update the forecast</p> | |
| <div class="row"> | |
| <div class="col-md-4"> | |
| <label class="form-label">D Margin</label> | |
| <input type="number" class="form-control" step="0.5"> | |
| </div> | |
| <div class="col-md-4"> | |
| <label class="form-label">Sample Size</label> | |
| <input type="number" class="form-control"> | |
| </div> | |
| <div class="col-md-4"> | |
| <label class="form-label">Pollster Rating</label> | |
| <select class="form-select"> | |
| <option>A+</option> | |
| <option>A</option> | |
| <option>B</option> | |
| <option>C</option> | |
| </select> | |
| </div> | |
| </div> | |
| <button class="btn btn-sm btn-primary mt-2"> | |
| <i class="bi bi-plus"></i> Add Poll | |
| </button> | |
| </div> | |
| <div id="early-vote-tab" class="tab-pane fade"> | |
| <p class="text-muted">Track early/mail ballots returned</p> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <label class="form-label">D Early Vote Share (%)</label> | |
| <input type="number" class="form-control" step="0.5"> | |
| </div> | |
| <div class="col-md-6"> | |
| <label class="form-label">% of Expected Turnout</label> | |
| <input type="number" class="form-control" step="1"> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-speedometer"></i> Current Forecast | |
| </div> | |
| <div class="card-body"> | |
| <div class="row text-center"> | |
| <div class="col-md-4"> | |
| <h1 class="text-primary">52.3%</h1> | |
| <p class="text-muted">Win Probability</p> | |
| </div> | |
| <div class="col-md-4"> | |
| <h1>D+1.8</h1> | |
| <p class="text-muted">Expected Margin</p> | |
| </div> | |
| <div class="col-md-4"> | |
| <h1 class="text-success">85%</h1> | |
| <p class="text-muted">Model Confidence</p> | |
| </div> | |
| </div> | |
| <div class="progress mt-3" style="height: 30px;"> | |
| <div class="progress-bar bg-primary" style="width: 52.3%">Democrat 52.3%</div> | |
| <div class="progress-bar bg-danger" style="width: 47.7%">Republican 47.7%</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Data Upload Section --> | |
| <div id="upload" class="content-section"> | |
| <h2>Data Upload & Management</h2> | |
| <p class="text-muted mb-4">Import voter files, election results, demographics, and precinct shapefiles</p> | |
| <div class="row"> | |
| <div class="col-lg-6"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-file-earmark-arrow-up"></i> Upload Election Results | |
| </div> | |
| <div class="card-body"> | |
| <div class="file-upload"> | |
| <i class="bi bi-cloud-upload"></i> | |
| <h5>Drag & Drop or Click to Upload</h5> | |
| <p class="text-muted">CSV, Excel, TXT (precinct-level results)</p> | |
| <input type="file" class="d-none" id="results-file" accept=".csv,.xlsx,.txt"> | |
| <button class="btn btn-primary mt-2" onclick="document.getElementById('results-file').click()"> | |
| Select File | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-lg-6"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-people"></i> Upload Voter File | |
| </div> | |
| <div class="card-body"> | |
| <div class="file-upload"> | |
| <i class="bi bi-cloud-upload"></i> | |
| <h5>Drag & Drop or Click to Upload</h5> | |
| <p class="text-muted">CSV, Excel (L2, TargetSmart, VAN export)</p> | |
| <input type="file" class="d-none" id="voter-file" accept=".csv,.xlsx"> | |
| <button class="btn btn-primary mt-2" onclick="document.getElementById('voter-file').click()"> | |
| Select File | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div class="col-lg-6"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-bar-chart"></i> Upload Demographics | |
| </div> | |
| <div class="card-body"> | |
| <div class="file-upload"> | |
| <i class="bi bi-cloud-upload"></i> | |
| <h5>Drag & Drop or Click to Upload</h5> | |
| <p class="text-muted">CSV, Excel (Census ACS tables)</p> | |
| <input type="file" class="d-none" id="demo-file" accept=".csv,.xlsx"> | |
| <button class="btn btn-primary mt-2" onclick="document.getElementById('demo-file').click()"> | |
| Select File | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-lg-6"> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-map"></i> Upload Precinct Boundaries | |
| </div> | |
| <div class="card-body"> | |
| <div class="file-upload"> | |
| <i class="bi bi-cloud-upload"></i> | |
| <h5>Drag & Drop or Click to Upload</h5> | |
| <p class="text-muted">GeoJSON, Shapefile (VEST, TIGER/Line)</p> | |
| <input type="file" class="d-none" id="geo-file" accept=".geojson,.json,.zip"> | |
| <button class="btn btn-primary mt-2" onclick="document.getElementById('geo-file').click()"> | |
| Select File | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-database"></i> Loaded Datasets | |
| </div> | |
| <div class="card-body"> | |
| <div class="alert alert-success"> | |
| <i class="bi bi-check-circle"></i> <strong>Sample District Data</strong> - 10 precincts, 2020-2024 results | |
| </div> | |
| <div class="alert alert-secondary"> | |
| <i class="bi bi-x-circle"></i> No voter file loaded | |
| </div> | |
| <div class="alert alert-secondary"> | |
| <i class="bi bi-x-circle"></i> No demographic data loaded | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Export Section --> | |
| <div id="export" class="content-section"> | |
| <h2>Export & Reports</h2> | |
| <p class="text-muted mb-4">Download analysis results, visualizations, and campaign reports</p> | |
| <div class="card"> | |
| <div class="card-header"> | |
| <i class="bi bi-file-earmark-text"></i> Available Exports | |
| </div> | |
| <div class="card-body"> | |
| <div class="list-group"> | |
| <div class="list-group-item d-flex justify-content-between align-items-center"> | |
| <div> | |
| <h6 class="mb-1">Precinct-Level Analysis (CSV)</h6> | |
| <small class="text-muted">All computed metrics, PVI, swing, turnout, strategic values</small> | |
| </div> | |
| <button class="btn btn-sm btn-primary"> | |
| <i class="bi bi-download"></i> Download | |
| </button> | |
| </div> | |
| <div class="list-group-item d-flex justify-content-between align-items-center"> | |
| <div> | |
| <h6 class="mb-1">Voter Targeting Matrix (Excel)</h6> | |
| <small class="text-muted">Segment-by-segment breakdown with strategies</small> | |
| </div> | |
| <button class="btn btn-sm btn-primary"> | |
| <i class="bi bi-download"></i> Download | |
| </button> | |
| </div> | |
| <div class="list-group-item d-flex justify-content-between align-items-center"> | |
| <div> | |
| <h6 class="mb-1">Monte Carlo Results (JSON)</h6> | |
| <small class="text-muted">Full simulation output with margin distribution</small> | |
| </div> | |
| <button class="btn btn-sm btn-primary"> | |
| <i class="bi bi-download"></i> Download | |
| </button> | |
| </div> | |
| <div class="list-group-item d-flex justify-content-between align-items-center"> | |
| <div> | |
| <h6 class="mb-1">Interactive Maps (HTML)</h6> | |
| <small class="text-muted">Standalone HTML files with all visualizations</small> | |
| </div> | |
| <button class="btn btn-sm btn-primary"> | |
| <i class="bi bi-download"></i> Download | |
| </button> | |
| </div> | |
| <div class="list-group-item d-flex justify-content-between align-items-center"> | |
| <div> | |
| <h6 class="mb-1">Campaign Strategy Report (PDF)</h6> | |
| <small class="text-muted">Executive summary with recommendations</small> | |
| </div> | |
| <button class="btn btn-sm btn-success"> | |
| <i class="bi bi-file-pdf"></i> Generate PDF | |
| </button> | |
| </div> | |
| <div class="list-group-item d-flex justify-content-between align-items-center"> | |
| <div> | |
| <h6 class="mb-1">GOTV Turf Packets (Printable)</h6> | |
| <small class="text-muted">Walk lists by precinct with maps</small> | |
| </div> | |
| <button class="btn btn-sm btn-success"> | |
| <i class="bi bi-printer"></i> Generate | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Bootstrap JS --> | |
| <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> | |
| <script> | |
| // Navigation | |
| document.querySelectorAll('.nav-item').forEach(item => { | |
| item.addEventListener('click', function() { | |
| // Update nav active state | |
| document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active')); | |
| this.classList.add('active'); | |
| // Show corresponding section | |
| const sectionId = this.dataset.section; | |
| document.querySelectorAll('.content-section').forEach(section => { | |
| section.classList.remove('active'); | |
| }); | |
| document.getElementById(sectionId).classList.add('active'); | |
| }); | |
| }); | |
| // Initialize Dashboard with Sample Data | |
| function initializeDashboard() { | |
| // Update metrics | |
| document.getElementById('win-prob').textContent = '58.3%'; | |
| document.getElementById('expected-margin').textContent = 'D+2.4'; | |
| document.getElementById('high-value-precincts').textContent = '8'; | |
| document.getElementById('contact-target').textContent = '24.5K'; | |
| // Sample scatter plot | |
| const sampleData = [ | |
| {x: -5, y: 3, name: 'P01', size: 200, color: 'Red Lean'}, | |
| {x: 2, y: 7, name: 'P02', size: 150, color: 'Swing'}, | |
| {x: 8, y: 2, name: 'P03', size: 180, color: 'Blue Base'}, | |
| {x: -2, y: 5, name: 'P04', size: 220, color: 'Swing'}, | |
| {x: 12, y: 4, name: 'P05', size: 160, color: 'Blue Base'}, | |
| ]; | |
| const trace = { | |
| x: sampleData.map(d => d.x), | |
| y: sampleData.map(d => d.y), | |
| text: sampleData.map(d => d.name), | |
| mode: 'markers', | |
| marker: { | |
| size: sampleData.map(d => d.size / 10), | |
| color: sampleData.map(d => d.color === 'Blue Base' ? '#1e40af' : d.color === 'Red Lean' ? '#dc2626' : '#9333ea') | |
| }, | |
| type: 'scatter' | |
| }; | |
| Plotly.newPlot('district-map', [trace], { | |
| title: 'Precinct Positioning', | |
| xaxis: {title: 'PVI (Partisan Lean)'}, | |
| yaxis: {title: 'Swing Score'}, | |
| hovermode: 'closest' | |
| }, {responsive: true}); | |
| // Demographic pie | |
| Plotly.newPlot('demo-pie', [{ | |
| values: [35, 28, 22, 15], | |
| labels: ['White Non-Hispanic', 'Hispanic/Latino', 'Black/AA', 'Asian/Other'], | |
| type: 'pie', | |
| marker: { | |
| colors: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6'] | |
| } | |
| }], { | |
| title: 'Racial/Ethnic Composition' | |
| }, {responsive: true}); | |
| // Win probability timeline | |
| const dates = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct']; | |
| const probs = [45, 48, 50, 52, 54, 56, 57, 58, 58, 58.3]; | |
| Plotly.newPlot('prob-timeline', [{ | |
| x: dates, | |
| y: probs, | |
| type: 'scatter', | |
| mode: 'lines+markers', | |
| line: {color: '#1e40af', width: 3}, | |
| fill: 'tozeroy' | |
| }], { | |
| yaxis: {title: 'Win Probability (%)', range: [0, 100]}, | |
| xaxis: {title: '2024'} | |
| }, {responsive: true}); | |
| } | |
| // Placeholder functions (would connect to backend/data processing) | |
| function generateDemoSegmentation() { | |
| alert('Generating demographic segmentation... | |
| This would analyze uploaded data and compute segment-level metrics.'); | |
| } | |
| function analyzePrecincts() { | |
| alert('Running precinct-level analysis... | |
| This would compute PVI, swing, elasticity, and clustering for all precincts.'); | |
| } | |
| function runMonteCarloSimulation() { | |
| document.getElementById('mc-spinner').classList.add('active'); | |
| setTimeout(() => { | |
| document.getElementById('mc-spinner').classList.remove('active'); | |
| document.getElementById('mc-win-prob').textContent = '58.3%'; | |
| document.getElementById('mc-margin').textContent = 'D+2.4'; | |
| document.getElementById('mc-recount').textContent = '3.2%'; | |
| // Generate histogram | |
| const margins = Array.from({length: 1000}, () => | |
| Math.random() * 10 - 3 | |
| ); | |
| Plotly.newPlot('mc-histogram', [{ | |
| x: margins, | |
| type: 'histogram', | |
| nbinsx: 50, | |
| marker: {color: '#1e40af'} | |
| }], { | |
| title: '10,000 Simulated Election Outcomes', | |
| xaxis: {title: 'D Margin (%)'}, | |
| yaxis: {title: 'Frequency'} | |
| }, {responsive: true}); | |
| }, 2000); | |
| } | |
| function resetSimulation() { | |
| document.getElementById('mc-win-prob').textContent = '--'; | |
| document.getElementById('mc-margin').textContent = '--'; | |
| document.getElementById('mc-recount').textContent = '--'; | |
| Plotly.purge('mc-histogram'); | |
| } | |
| function runScenario() { | |
| alert('Calculating scenario... | |
| This would re-run the model with adjusted demographic/turnout parameters.'); | |
| } | |
| function buildUniverse() { | |
| document.getElementById('universe-size').textContent = '24,518'; | |
| document.getElementById('contact-rate').textContent = '65%'; | |
| document.getElementById('vote-yield').textContent = '+1,842'; | |
| alert('Universe generated! | |
| 24,518 voters identified across persuasion and mobilization targets.'); | |
| } | |
| function optimizeTurf() { | |
| alert('Optimizing turf allocation... | |
| This would rank precincts and allocate volunteer resources to maximize vote yield.'); | |
| } | |
| // Initialize on load | |
| window.addEventListener('load', initializeDashboard); | |
| </script> | |
| </body> | |
| </html> |