@import url(‘https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@300;400;600;700&display=swap’);
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: ‘Rajdhani’, sans-serif;
background: linear-gradient(135deg, #0c0c0c 0%, #1a1a2e 25%, #16213e 50%, #0f3460 75%, #533483 100%);
color: #e0e0e0;
min-height: 100vh;
overflow-x: hidden;
}
.cyber-grid {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
linear-gradient(rgba(0, 255, 255, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 255, 255, 0.03) 1px, transparent 1px);
background-size: 50px 50px;
z-index: -1;
animation: gridPulse 4s ease-in-out infinite;
}
@keyframes gridPulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 0.6; }
}
.container {
/* CHANGED: Removed max-width to allow it to expand */
width: 95%; /* Keeps a small margin on the very edges */
margin: 0 auto;
padding: 30px; /* Increased padding for better internal spacing */
position: relative;
z-index: 1;
}
.header {
text-align: center;
padding: 40px 20px;
background: linear-gradient(135deg, rgba(0, 255, 255, 0.1) 0%, rgba(255, 0, 255, 0.1) 100%);
border: 1px solid rgba(0, 255, 255, 0.3);
border-radius: 20px;
margin-bottom: 30px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 255, 255, 0.1);
position: relative;
overflow: hidden;
}
.header::before {
content: ”;
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(0, 255, 255, 0.05), transparent);
animation: shimmer 3s linear infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
}
.header h1 {
font-family: ‘Orbitron’, monospace;
font-size: 3.5rem;
font-weight: 900;
background: linear-gradient(135deg, #00ffff 0%, #ff00ff 50%, #00ff00 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 30px rgba(0, 255, 255, 0.5);
margin-bottom: 10px;
position: relative;
z-index: 2;
}
.header p {
font-size: 1.3rem;
color: #a0a0a0;
position: relative;
z-index: 2;
margin-bottom: 20px; /* Added for spacing */
}
.header .intro-text {
font-size: 1.1rem;
color: #c0c0c0;
max-width: 800px;
margin: 0 auto;
line-height: 1.6;
}
.main-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 25px;
margin-bottom: 30px;
}
.panel {
background: linear-gradient(135deg, rgba(0, 0, 0, 0.8) 0%, rgba(20, 20, 40, 0.9) 100%);
border: 1px solid rgba(0, 255, 255, 0.2);
border-radius: 15px;
padding: 25px;
backdrop-filter: blur(10px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.panel:hover {
border-color: rgba(0, 255, 255, 0.4);
box-shadow: 0 12px 40px rgba(0, 255, 255, 0.1);
transform: translateY(-2px);
}
.panel::before {
content: ”;
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, #00ffff, transparent);
animation: scanLine 2s linear infinite;
}
@keyframes scanLine {
0% { left: -100%; }
100% { left: 100%; }
}
.panel-title {
font-family: ‘Orbitron’, monospace;
font-size: 1.4rem;
font-weight: 700;
color: #00ffff;
margin-bottom: 20px;
text-transform: uppercase;
letter-spacing: 2px;
display: flex;
align-items: center;
gap: 10px;
}
.panel-title::before {
content: ”;
width: 8px;
height: 8px;
background: #00ffff;
border-radius: 50%;
box-shadow: 0 0 10px #00ffff;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(1.2); }
}
.input-group {
margin-bottom: 20px;
}
.input-label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #b0b0b0;
font-size: 1.1rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.cyber-input {
width: 100%;
padding: 12px 15px;
background: rgba(0, 0, 0, 0.6);
border: 2px solid rgba(0, 255, 255, 0.3);
border-radius: 8px;
color: #00ffff;
font-size: 1.1rem;
font-family: ‘Rajdhani’, sans-serif;
transition: all 0.3s ease;
outline: none;
}
.cyber-input:focus {
border-color: #00ffff;
box-shadow: 0 0 20px rgba(0, 255, 255, 0.3);
background: rgba(0, 255, 255, 0.05);
}
.cyber-select {
width: 100%;
padding: 12px 15px;
background: rgba(0, 0, 0, 0.6);
border: 2px solid rgba(0, 255, 255, 0.3);
border-radius: 8px;
color: #00ffff;
font-size: 1.1rem;
font-family: ‘Rajdhani’, sans-serif;
cursor: pointer;
outline: none;
}
.candidate-section {
grid-column: 1 / -1;
background: linear-gradient(135deg, rgba(0, 0, 0, 0.9) 0%, rgba(30, 30, 60, 0.9) 100%);
border: 1px solid rgba(255, 0, 255, 0.2);
border-radius: 15px;
padding: 30px;
margin-top: 20px;
}
.candidate-grid {
display: grid;
grid-template-columns: 2fr repeat(4, 1fr) auto;
gap: 15px;
align-items: center;
margin-bottom: 15px;
padding: 15px;
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 0, 255, 0.1);
border-radius: 10px;
transition: all 0.3s ease;
}
.candidate-grid:hover {
border-color: rgba(255, 0, 255, 0.3);
background: rgba(255, 0, 255, 0.05);
}
.grade-header {
display: grid;
grid-template-columns: 2fr repeat(4, 1fr) auto;
gap: 15px;
margin-bottom: 10px;
padding: 10px 15px;
background: rgba(255, 0, 255, 0.1);
border-radius: 8px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: #ff00ff;
}
.cyber-btn {
background: linear-gradient(135deg, #00ffff 0%, #ff00ff 100%);
color: #000;
border: none;
padding: 15px 30px;
border-radius: 8px;
font-family: ‘Orbitron’, monospace;
font-weight: 700;
font-size: 1.1rem;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 1px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.cyber-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 30px rgba(0, 255, 255, 0.3);
}
.cyber-btn::before {
content: ”;
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.5s;
}
.cyber-btn:hover::before {
left: 100%;
}
.cyber-btn-small {
padding: 8px 15px;
font-size: 0.9rem;
}
.cyber-btn-danger {
background: linear-gradient(135deg, #ff0040 0%, #ff4080 100%);
color: white;
}
.stats-display {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin: 20px 0;
}
.stat-card {
background: rgba(0, 0, 0, 0.8);
border: 1px solid rgba(0, 255, 255, 0.3);
border-radius: 10px;
padding: 20px;
text-align: center;
position: relative;
overflow: hidden;
}
.stat-card::before {
content: ”;
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(45deg, #00ffff, #ff00ff, #00ff00, #ffff00);
border-radius: 10px;
z-index: -1;
animation: borderRotate 3s linear infinite;
}
@keyframes borderRotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.stat-number {
font-family: ‘Orbitron’, monospace;
font-size: 2.5rem;
font-weight: 900;
color: #00ffff;
margin-bottom: 5px;
text-shadow: 0 0 10px rgba(0, 255, 255, 0.5);
}
.stat-label {
font-size: 0.9rem;
color: #a0a0a0;
text-transform: uppercase;
letter-spacing: 1px;
}
.results-section {
margin-top: 30px;
display: none;
}
.charts-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 25px;
margin: 25px 0;
}
.chart-panel {
background: linear-gradient(135deg, rgba(0, 0, 0, 0.9) 0%, rgba(20, 20, 40, 0.9) 100%);
border: 1px solid rgba(0, 255, 255, 0.2);
border-radius: 15px;
padding: 25px;
position: relative;
overflow: hidden;
}
.chart-title {
font-family: ‘Orbitron’, monospace;
font-size: 1.2rem;
color: #ff00ff;
margin-bottom: 10px; /* Reduced for explainer */
text-transform: uppercase;
letter-spacing: 1px;
}
.chart-explainer {
font-size: 0.9rem;
color: #909090;
margin-bottom: 15px;
line-height: 1.4;
}
.results-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
background: rgba(0, 0, 0, 0.8);
border-radius: 10px;
overflow: hidden;
}
.results-table th {
background: linear-gradient(135deg, #00ffff 0%, #ff00ff 100%);
color: #000;
padding: 15px;
font-family: ‘Orbitron’, monospace;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
}
.results-table td {
padding: 12px 15px;
border-bottom: 1px solid rgba(0, 255, 255, 0.1);
color: #e0e0e0;
}
.results-table tbody tr:hover {
background: rgba(0, 255, 255, 0.05);
}
.warning {
background: rgba(255, 165, 0, 0.1);
border: 1px solid rgba(255, 165, 0, 0.3);
color: #ffb84d;
padding: 15px;
border-radius: 8px;
margin: 15px 0;
}
.success {
background: rgba(0, 255, 0, 0.1);
border: 1px solid rgba(0, 255, 0, 0.3);
color: #4dff4d;
padding: 15px;
border-radius: 8px;
margin: 15px 0;
}
canvas {
max-width: 100%;
height: 350px !important;
}
.loading {
display: none;
text-align: center;
padding: 40px;
color: #00ffff;
font-size: 1.2rem;
}
.loading::before {
content: ”;
display: inline-block;
width: 40px;
height: 40px;
border: 4px solid rgba(0, 255, 255, 0.3);
border-top: 4px solid #00ffff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@media (max-width: 1200px) {
.main-grid {
grid-template-columns: 1fr 1fr;
}
.candidate-grid, .grade-header {
grid-template-columns: 1.5fr repeat(4, 0.8fr) auto;
font-size: 0.9rem;
}
}
@media (max-width: 768px) {
.main-grid {
grid-template-columns: 1fr;
}
.header h1 {
font-size: 2.5rem;
}
.candidate-grid, .grade-header {
grid-template-columns: 1fr;
gap: 10px;
text-align: left;
}
.charts-grid {
grid-template-columns: 1fr;
}
}
⚡ NEURAL ELECTION SIMULATOR
Advanced Monte Carlo Analysis with Grade-Specific Demographics
Configure student populations, voter turnout, and candidate popularities across different grades.
The simulator runs multiple Monte Carlo trials to project election outcomes, predict winners,
and visualize potential vote distributions and probabilities.
100 (Fast)
500 (Balanced)
1,000 (Detailed)
5,000 (Neural)
10,000 (Quantum)
Higher variance means more random fluctuations in votes per simulation.
These stats update automatically based on your student population and turnout inputs.
Input each candidate’s popularity percentage for each grade.
Ensure percentages for each grade sum to 100% across all candidates.
This bar chart shows the average number of votes each candidate received across all simulation runs.
It gives you a clear picture of typical vote counts.
A pie chart illustrating each candidate’s probability of winning the election,
based on the percentage of simulations they won.
This stacked bar chart displays the average votes each candidate secured
from each grade level, highlighting their popularity within specific demographics.
Shows the standard deviation of votes for each candidate across all simulations.
A higher bar indicates more fluctuation in vote counts for that candidate.
A histogram showing the distribution of victory margins (difference between winner’s votes and runner-up’s votes)
across all simulations where a clear winner emerged.
Tracks the win rate of the leading candidate over the course of the simulations.
A flattening curve indicates the simulation is converging and results are stabilizing.
| CANDIDATE | AVG VOTES | VOTE RANGE | WIN RATE | VOTE SHARE | GRADE 9 AVG | GRADE 10 AVG | GRADE 11 AVG | GRADE 12 AVG |
|---|
let charts = {}; // Object to hold Chart.js instances
// Initial update of total students and projected voters
document.addEventListener(‘DOMContentLoaded’, () => {
updateTotalStudents();
addEventListeners(); // Add listeners for initial candidates
checkPopularityTotals(); // Check initial candidate popularity
});
function updateTotalStudents() {
const grade9 = parseInt(document.getElementById(‘grade9’).value) || 0;
const grade10 = parseInt(document.getElementById(‘grade10’).value) || 0;
const grade11 = parseInt(document.getElementById(‘grade11’).value) || 0;
const grade12 = parseInt(document.getElementById(‘grade12’).value) || 0;
const total = grade9 + grade10 + grade11 + grade12;
const turnout = parseInt(document.getElementById(‘turnout’).value) || 75;
const voters = Math.round(total * turnout / 100);
document.getElementById(‘totalStudents’).textContent = total.toLocaleString();
document.getElementById(‘totalVoters’).textContent = voters.toLocaleString();
}
function checkPopularityTotals() {
const candidates = document.querySelectorAll(‘#candidatesList .candidate-grid’);
const grades = [‘Grade 9’, ‘Grade 10’, ‘Grade 11’, ‘Grade 12’];
let warnings = [];
for (let gradeIndex = 0; gradeIndex {
const inputs = row.querySelectorAll(‘input[type=”number”]’);
// Ensure the input exists for the given grade index before trying to get its value
if (inputs[gradeIndex]) { // The input indices here correctly map to popularity inputs
total += parseInt(inputs[gradeIndex].value) || 0;
}
});
// Allow for slight floating point inaccuracies if any, otherwise strict 100
if (Math.abs(total – 100) > 0.1) {
warnings.push(`${grades[gradeIndex]}: ${total}%`);
}
}
const warning = document.getElementById(‘popularityWarning’);
if (warnings.length > 0) {
warning.style.display = ‘block’;
warning.innerHTML = `⚠️ Popularity totals should sum to 100% per grade. Current totals: ${warnings.join(‘ | ‘)}`;
} else {
warning.style.display = ‘none’;
}
}
function addCandidate() {
const container = document.getElementById(‘candidatesList’);
const newRow = document.createElement(‘div’);
newRow.className = ‘candidate-grid’;
newRow.innerHTML = `
`;
container.appendChild(newRow);
addEventListeners(); // Re-add listeners for new inputs
checkPopularityTotals(); // Re-check totals after adding
}
function removeCandidate(button) {
const row = button.closest(‘.candidate-grid’);
row.remove();
checkPopularityTotals(); // Re-check totals after removing
}
function resetCandidates() {
const container = document.getElementById(‘candidatesList’);
container.innerHTML = `
`;
addEventListeners(); // Re-add listeners for new inputs
checkPopularityTotals(); // Re-check totals after reset
}
function addEventListeners() {
// Remove existing listeners to prevent duplicates
document.querySelectorAll(‘#grade9, #grade10, #grade11, #grade12, #turnout’)
.forEach(input => input.removeEventListener(‘input’, updateTotalStudents));
// Add new listeners
document.querySelectorAll(‘#grade9, #grade10, #grade11, #grade12, #turnout’)
.forEach(input => input.addEventListener(‘input’, updateTotalStudents));
// Remove existing listeners for candidate popularity inputs
document.querySelectorAll(‘#candidatesList .candidate-grid input[type=”number”]’)
.forEach(input => input.removeEventListener(‘input’, checkPopularityTotals));
// Add new listeners for candidate popularity inputs
document.querySelectorAll(‘#candidatesList .candidate-grid input[type=”number”]’)
.forEach(input => input.addEventListener(‘input’, checkPopularityTotals));
}
async function runSimulation() {
const loadingIndicator = document.getElementById(‘loadingIndicator’);
const resultsSection = document.getElementById(‘resultsSection’);
const resultsMessage = document.getElementById(‘resultsMessage’);
loadingIndicator.style.display = ‘block’;
resultsSection.style.display = ‘none’;
resultsMessage.textContent = ”; // Clear previous messages
const gradePopulations = {
grade9: parseInt(document.getElementById(‘grade9’).value) || 0,
grade10: parseInt(document.getElementById(‘grade10’).value) || 0,
grade11: parseInt(document.getElementById(‘grade11’).value) || 0,
grade12: parseInt(document.getElementById(‘grade12’).value) || 0
};
const turnout = parseInt(document.getElementById(‘turnout’).value) || 75;
const numSimulations = parseInt(document.getElementById(‘simulations’).value) || 500;
const varianceFactor = parseInt(document.getElementById(‘variance’).value) || 8;
const candidateRows = document.querySelectorAll(‘#candidatesList .candidate-grid’);
const candidates = Array.from(candidateRows).map(row => {
const inputs = row.querySelectorAll(‘input’);
return {
name: inputs[0].value || ‘Unknown Candidate’,
popularity: {
9: (parseInt(inputs[1].value) || 0),
10: (parseInt(inputs[2].value) || 0),
11: (parseInt(inputs[3].value) || 0),
12: (parseInt(inputs[4].value) || 0),
}
};
});
// Frontend validation for popularity totals (important to keep)
let popularityValid = true;
for (let gradeIndex = 0; gradeIndex {
const gradeKey = (9 + gradeIndex).toString();
total += cand.popularity[gradeKey];
});
if (Math.abs(total – 100) > 0.1) {
popularityValid = false;
break;
}
}
if (!popularityValid) {
loadingIndicator.style.display = ‘none’;
resultsSection.style.display = ‘block’;
resultsMessage.className = ‘warning’;
resultsMessage.innerHTML = ‘⚠️ **ERROR:** Popularity percentages for each grade must sum to 100%. Please correct the candidate percentages.’;
return;
}
if (candidates.length === 0) {
loadingIndicator.style.display = ‘none’;
resultsSection.style.display = ‘block’;
resultsMessage.className = ‘warning’;
resultsMessage.innerHTML = ‘⚠️ **ERROR:** Please add at least one candidate to run the simulation.’;
return;
}
try {
// In a real application, this would fetch from a backend server.
// For this self-contained HTML, we’ll simulate the backend logic here.
const results = simulateElection(gradePopulations, turnout, numSimulations, varianceFactor, candidates);
// Update display stats
document.getElementById(‘totalVotesCast’).textContent = results.total_votes_cast.toFixed(0).toLocaleString();
document.getElementById(‘winnerName’).textContent = results.winner_name;
document.getElementById(‘winProbability’).textContent = results.win_probability.toFixed(2) + ‘%’;
document.getElementById(‘avgMargin’).textContent = results.avg_margin.toFixed(0);
// Populate results table
const resultsTableBody = document.getElementById(‘resultsTableBody’);
resultsTableBody.innerHTML = ”; // Clear previous results
results.candidate_results.forEach(cand => {
const row = resultsTableBody.insertRow();
row.insertCell().textContent = cand.name;
row.insertCell().textContent = cand.avg_votes.toFixed(0).toLocaleString();
row.insertCell().textContent = cand.vote_range;
row.insertCell().textContent = `${cand.win_rate.toFixed(2)}%`;
row.insertCell().textContent = `${cand.vote_share.toFixed(2)}%`;
row.insertCell().textContent = cand.grade_9_avg.toFixed(0).toLocaleString();
row.insertCell().textContent = cand.grade_10_avg.toFixed(0).toLocaleString();
row.insertCell().textContent = cand.grade_11_avg.toFixed(0).toLocaleString();
row.insertCell().textContent = cand.grade_12_avg.toFixed(0).toLocaleString();
});
// Chart Data Preparation
const candidateNames = results.candidate_names;
const candidateColors = [‘#00ffff’, ‘#ff00ff’, ‘#00ff00’, ‘#ffff00’, ‘#ff8c00’, ‘#8a2be2’]; // More colors if needed
// Vote Distribution Chart
updateChart(‘voteChart’, ‘bar’, {
labels: candidateNames,
datasets: [{
label: ‘Average Votes’,
data: results.vote_distribution_data,
backgroundColor: candidateColors.slice(0, candidateNames.length),
borderColor: candidateColors.slice(0, candidateNames.length).map(c => c + ’80’),
borderWidth: 1
}]
}, {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: { display: true, text: ‘Average Votes’ }
}
}
});
// Win Probability Chart
updateChart(‘probabilityChart’, ‘pie’, {
labels: candidateNames,
datasets: [{
label: ‘Win Probability’,
data: results.win_prob_data,
backgroundColor: candidateColors.slice(0, candidateNames.length),
hoverOffset: 4
}]
}, {
responsive: true,
maintainAspectRatio: false,
plugins: {
tooltip: {
callbacks: {
label: function(context) {
let label = context.label || ”;
if (label) {
label += ‘: ‘;
}
if (context.parsed !== null) {
label += context.parsed.toFixed(2) + ‘%’;
}
return label;
}
}
}
}
});
// Grade-Level Breakdown Chart
const gradeBreakdownDatasets = candidates.map((cand, idx) => ({
label: cand.name,
data: results.grade_breakdown_data[cand.name].map(val => val.toFixed(0)),
backgroundColor: candidateColors[idx % candidateColors.length],
borderColor: candidateColors[idx % candidateColors.length],
borderWidth: 1,
fill: false
}));
updateChart(‘gradeChart’, ‘bar’, {
labels: [‘Grade 9’, ‘Grade 10’, ‘Grade 11’, ‘Grade 12’],
datasets: gradeBreakdownDatasets
}, {
responsive: true,
maintainAspectRatio: false,
scales: {
x: { stacked: true },
y: { stacked: true, beginAtZero: true, title: { display: true, text: ‘Average Votes per Grade’ } }
}
});
// Simulation Variance Chart
updateChart(‘varianceChart’, ‘bar’, {
labels: candidateNames,
datasets: [{
label: ‘Vote Std Dev’,
data: results.variance_data,
backgroundColor: candidateColors.slice(0, candidateNames.length).map(c => c + ’60’),
borderColor: candidateColors.slice(0, candidateNames.length),
borderWidth: 1
}]
}, {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true,
title: { display: true, text: ‘Standard Deviation of Votes’ }
}
}
});
// Victory Margin Distribution Chart
updateChart(‘marginChart’, ‘bar’, {
labels: results.margin_chart_data.labels,
datasets: [{
label: ‘Number of Simulations’,
data: results.margin_chart_data.data,
backgroundColor: ‘#ff8c0060’,
borderColor: ‘#ff8c00’,
borderWidth: 1
}]
}, {
responsive: true,
maintainAspectRatio: false,
scales: {
x: { title: { display: true, text: ‘Victory Margin (Votes)’ } },
y: { beginAtZero: true, title: { display: true, text: ‘Frequency’ } }
}
});
// Monte Carlo Convergence Chart
updateChart(‘convergenceChart’, ‘line’, {
labels: results.convergence_data.map(d => d.iteration),
datasets: [{
label: ‘Leading Candidate Win Rate (%)’,
data: results.convergence_data.map(d => d.win_rate),
borderColor: ‘#00ccff’,
backgroundColor: ‘#00ccff20’,
fill: true,
tension: 0.1
}]
}, {
responsive: true,
maintainAspectRatio: false,
scales: {
x: { title: { display: true, text: ‘Simulation Iteration’ } },
y: { beginAtZero: true, max: 100, title: { display: true, text: ‘Win Rate (%)’ } }
}
});
resultsMessage.className = ‘success’;
resultsMessage.innerHTML = ‘✅ Simulation Complete! Analyze the neural predictions below.’;
} catch (error) {
console.error(‘Simulation error:’, error);
resultsMessage.className = ‘warning’;
resultsMessage.innerHTML = `⚠️ **ERROR:** ${error.message}`;
} finally {
loadingIndicator.style.display = ‘none’;
resultsSection.style.display = ‘block’;
}
}
function updateChart(canvasId, type, data, options) {
const ctx = document.getElementById(canvasId).getContext(‘2d’);
if (charts[canvasId]) {
charts[canvasId].destroy(); // Destroy previous chart instance
}
charts[canvasId] = new Chart(ctx, {
type: type,
data: data,
options: options
});
}
// — BACKEND SIMULATION LOGIC (Copied from previous Python backend) —
// This is a simplified JavaScript translation of the core simulation.
// It’s placed here to make the HTML file self-contained for online compilers.
function gaussianRandom(mean, stdDev) {
let u = 0, v = 0;
while (u === 0) u = Math.random(); // Converting [0,1) to (0,1)
while (v === 0) v = Math.random();
let num = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
return num * stdDev + mean;
}
function simulateElection(gradePopulations, turnout, numSimulations, varianceFactor, candidates) {
const grades = [‘9′, ’10’, ’11’, ’12’];
const candidateNames = candidates.map(c => c.name);
let simulationResults = []; // Stores vote counts for each candidate per simulation
let winnerCounts = {}; // Stores how many times each candidate won
candidateNames.forEach(name => winnerCounts[name] = 0);
let convergenceData = [];
let leadingCandidateWins = 0;
for (let sim = 0; sim currentSimVotes[name] = 0);
let totalVotesThisSim = 0;
let votesByGrade = {}; // {candidateName: {grade: votes}}
grades.forEach(grade => {
const gradePop = gradePopulations[`grade${grade}`];
const numVoters = Math.round(gradePop * turnout / 100);
totalVotesThisSim += numVoters;
// Calculate a base percentage for each candidate in this grade
let basePercentages = {};
candidates.forEach(cand => {
basePercentages[cand.name] = cand.popularity[grade] / 100;
});
// Distribute votes with variance
let gradeVotesDistributed = 0;
let candidateGradeVotes = {};
// Generate votes for each candidate in this grade
let rawVotes = {};
let sumRawVotes = 0;
candidates.forEach(cand => {
const meanVotes = numVoters * basePercentages[cand.name];
const stdDev = (varianceFactor / 100) * meanVotes; // Variance as a percentage of mean
let votes = Math.max(0, Math.round(gaussianRandom(meanVotes, stdDev)));
rawVotes[cand.name] = votes;
sumRawVotes += votes;
});
// Normalize votes to total ‘numVoters’ for the grade
candidates.forEach(cand => {
if (sumRawVotes > 0) {
candidateGradeVotes[cand.name] = Math.round(rawVotes[cand.name] / sumRawVotes * numVoters);
} else {
candidateGradeVotes[cand.name] = 0;
}
currentSimVotes[cand.name] += candidateGradeVotes[cand.name];
gradeVotesDistributed += candidateGradeVotes[cand.name];
});
// Adjust for any rounding errors to make total sum exactly numVoters
let diff = numVoters – gradeVotesDistributed;
if (diff !== 0 && candidates.length > 0) {
// Distribute remaining difference to candidates with highest votes
let sortedCandidates = Object.keys(candidateGradeVotes).sort((a, b) => candidateGradeVotes[b] – candidateGradeVotes[a]);
for (let i = 0; i
candidateGradeVotes[sortedCandidates[i % sortedCandidates.length]]++;
} else {
candidateGradeVotes[sortedCandidates[i % sortedCandidates.length]]–;
}
}
// Update currentSimVotes from corrected gradeVotes
candidates.forEach(cand => {
currentSimVotes[cand.name] = (currentSimVotes[cand.name] – gradeVotesDistributed) + candidateGradeVotes[cand.name];
});
}
votesByGrade[grade] = candidateGradeVotes; // Store grade-specific votes
});
simulationResults.push({
votes: currentSimVotes,
gradeVotes: votesByGrade,
totalVotes: totalVotesThisSim // Sum of votes cast in this simulation
});
// Update winner counts for convergence
let maxVotes = -1;
let currentWinner = null;
candidateNames.forEach(name => {
if (currentSimVotes[name] > maxVotes) {
maxVotes = currentSimVotes[name];
currentWinner = name;
}
});
// Check for ties in this simulation
let tie = false;
candidateNames.forEach(name => {
if (name !== currentWinner && currentSimVotes[name] === maxVotes) {
tie = true;
}
});
if (!tie && currentWinner) {
winnerCounts[currentWinner]++;
}
// Convergence data: Win rate of the current leading candidate
if (sim % 10 === 0 || sim === numSimulations – 1) { // Log every 10 simulations or at the end
let currentLeader = null;
let leaderWinRate = 0;
let maxWins = -1;
Object.keys(winnerCounts).forEach(candName => {
if (winnerCounts[candName] > maxWins) {
maxWins = winnerCounts[candName];
currentLeader = candName;
}
});
if (sim + 1 > 0 && currentLeader) {
leaderWinRate = (winnerCounts[currentLeader] / (sim + 1)) * 100;
}
convergenceData.push({
iteration: sim + 1,
win_rate: leaderWinRate
});
}
}
// Calculate overall results
let avgVotes = {};
let minMaxVotes = {};
let winRates = {};
let voteShares = {};
let avgGradeVotes = {}; // {candidateName: {grade: avgVotes}}
candidateNames.forEach(name => {
avgVotes[name] = 0;
minMaxVotes[name] = { min: Infinity, max: -Infinity };
winRates[name] = (winnerCounts[name] / numSimulations) * 100;
avgGradeVotes[name] = { ‘9’: 0, ’10’: 0, ’11’: 0, ’12’: 0 };
});
let totalAvgVotesCast = 0;
simulationResults.forEach(simRes => {
candidateNames.forEach(name => {
avgVotes[name] += simRes.votes[name];
minMaxVotes[name].min = Math.min(minMaxVotes[name].min, simRes.votes[name]);
minMaxVotes[name].max = Math.max(minMaxVotes[name].max, simRes.votes[name]);
grades.forEach(grade => {
avgGradeVotes[name][grade] += simRes.gradeVotes[grade][name];
});
});
totalAvgVotesCast += simRes.totalVotes;
});
candidateNames.forEach(name => {
avgVotes[name] /= numSimulations;
grades.forEach(grade => {
avgGradeVotes[name][grade] /= numSimulations;
});
});
totalAvgVotesCast /= numSimulations;
// Calculate vote shares
candidateNames.forEach(name => {
if (totalAvgVotesCast > 0) {
voteShares[name] = (avgVotes[name] / totalAvgVotesCast) * 100;
} else {
voteShares[name] = 0;
}
});
// Determine overall winner and win probability
let overallWinner = ‘No clear winner’;
let maxWinRate = 0;
candidateNames.forEach(name => {
if (winRates[name] > maxWinRate) {
maxWinRate = winRates[name];
overallWinner = name;
} else if (winRates[name] === maxWinRate && name !== overallWinner) {
overallWinner = ‘Too close to call’; // Handle ties in overall win rate
}
});
// Calculate average margin
let totalMargin = 0;
let marginCount = 0;
simulationResults.forEach(simRes => {
const sortedVotes = Object.values(simRes.votes).sort((a, b) => b – a);
if (sortedVotes.length >= 2) {
totalMargin += (sortedVotes[0] – sortedVotes[1]);
marginCount++;
}
});
const avgMargin = marginCount > 0 ? totalMargin / marginCount : 0;
// Prepare results for charts
const voteDistributionData = candidateNames.map(name => avgVotes[name]);
const winProbData = candidateNames.map(name => winRates[name]);
let gradeBreakdownData = {};
candidateNames.forEach(name => {
gradeBreakdownData[name] = grades.map(grade => avgGradeVotes[name][grade]);
});
// Calculate Variance (Standard Deviation)
let varianceData = candidateNames.map(name => {
const votesForCandidate = simulationResults.map(res => res.votes[name]);
const n = votesForCandidate.length;
if (n a + b, 0) / n;
const sumOfSquares = votesForCandidate.reduce((sum, val) => sum + Math.pow(val – mean, 2), 0);
return Math.sqrt(sumOfSquares / n); // Population standard deviation
});
// Victory Margin Distribution Data (Histogram Bins)
const marginBins = {};
const marginBinSize = 50; // Group margins into bins of 50
simulationResults.forEach(simRes => {
const sortedVotes = Object.values(simRes.votes).sort((a, b) => b – a);
if (sortedVotes.length >= 2) {
const margin = sortedVotes[0] – sortedVotes[1];
const bin = Math.floor(margin / marginBinSize) * marginBinSize;
marginBins[bin] = (marginBins[bin] || 0) + 1;
}
});
const sortedMarginBins = Object.keys(marginBins).sort((a, b) => parseInt(a) – parseInt(b));
const marginChartLabels = sortedMarginBins.map(bin => `${bin}-${parseInt(bin) + marginBinSize – 1}`);
const marginChartData = sortedMarginBins.map(bin => marginBins[bin]);
let candidateResults = candidateNames.map(name => ({
name: name,
avg_votes: avgVotes[name],
vote_range: `${minMaxVotes[name].min.toFixed(0)} – ${minMaxVotes[name].max.toFixed(0)}`,
win_rate: winRates[name],
vote_share: voteShares[name],
grade_9_avg: avgGradeVotes[name][‘9′],
grade_10_avg: avgGradeVotes[name][’10’],
grade_11_avg: avgGradeVotes[name][’11’],
grade_12_avg: avgGradeVotes[name][’12’]
}));
return {
total_votes_cast: totalAvgVotesCast,
winner_name: overallWinner,
win_probability: maxWinRate,
avg_margin: avgMargin,
candidate_names: candidateNames,
vote_distribution_data: voteDistributionData,
win_prob_data: winProbData,
grade_breakdown_data: gradeBreakdownData,
variance_data: varianceData,
margin_chart_data: {
labels: marginChartLabels,
data: marginChartData
},
convergence_data: convergenceData,
candidate_results: candidateResults
};
}
// — END BACKEND SIMULATION LOGIC —
// Initialize event listeners when the page loads
document.addEventListener(‘DOMContentLoaded’, addEventListeners);
document.addEventListener(‘DOMContentLoaded’, updateTotalStudents);
document.addEventListener(‘DOMContentLoaded’, checkPopularityTotals);
