Who will win? In some Jackson council races, not even seasoned politicos know, but one thing is for sure: It all comes down to who votes.
Conventional wisdom holds that in Jackson, race, class and political party are linked. The city’s population is roughly 82% Black and 14% white, with Black Jacksonians historically backing Democrats. Other races make up the remaining 4%.
In the following interactive, you can test your hypothesis of where each candidate’s votes will come from and view the election results based on their support.
To use the tool, type in the portion of the total turnout you think will be made up by Black and white voters in each race and the portion of the Black and white vote you think each candidate will receive to make a result projection. Toggle between elections at the top of this interactive to make projections in council races for Ward 1 and Ward 7.
Some caveats before you get started: The following interactive only deals with turnout by Black and white Jacksonians. We don’t have data to quantify the race of Jackson’s registered voters or the rate of turnout among Black and white voters in past elections.
Considering that turnout is linked with socioeconomic status and has historically been higher in Ward 1 — Jackson’s whitest and wealthiest ward — compared to the rest of the city, you might make the assumption that whites will make up a larger portion of the city’s turnout than their population rate. But how much will that affect the outcome?
With Black residents outnumbering white residents more than 5-t0-1 citywide, higher turnout among affluent whites does not necessarily translate to electoral success for their candidate. But in Wards 1 and 7, where Black residents only slightly outnumber white, voting along racial lines takes center stage.
Your projection totals for the portion of the vote each candidate receives must add up to 100%. Citywide, experts say Black voters could make up between 65-85% of the turnout.
Mayor’s race: Jorhn Horhn (D), Kenneth Gee (R), Rodney DePriest (I), Zach Servis (I), Lillie Stewart-Robinson (I) and Kim Wade* (I)
| Who votes? | Your projections |
|---|---|
| Portion of the vote cast by each race | {{sb+sw == 1}} |
| Black | {{sb=85}} |
| White | {{sw=15}} |
| Portion of the Black vote each candidate receives | {{b1+b2+b3+b4+b5+b6 == 1}} |
| Horhn | {{b1=0}} |
| Gee | {{b2=0}} |
| DePriest | {{b3=0}} |
| Servis | {{b4=0}} |
| Stewart-Robinson | {{b5=0}} |
| Wade* | {{b6=0}} |
| Portion of the white vote each candidate receives | {{w1+w2+w3+w4+w5+w6 == 1}} |
| Horhn | {{w1=0}} |
| Gee | {{w2=0}} |
| DePriest | {{w3=0}} |
| Servis | {{w4=0}} |
| Stewart-Robinson | {{w5=0}} |
| Wade* | {{w6=0}} |
| Your projected results | |
| Horhn | {{sb*b1 + sw*w1}} |
| Gee | {{sb*b2 + sw*w2}} |
| DePriest | {{sb*b3 + sw*w3}} |
| Servis | {{sb*b4 + sw*w4}} |
| Stewart-Robinson | {{sb*b5 + sw*w5}} |
| Wade* | {{sb*b6 + sw*w6}} |
Your projection totals for the portion of the vote each candidate receives must add up to 100%. In Ward 1, experts say Black voters could make up between 40-60% of the turnout.
Ward 1 council race: Jasmine Barnes (D), Ashby Foote (I), Grace Greene (I)
| Base Assumptions | You decide |
|---|---|
| Portion of the vote cast by each race | {{sb+sw == 1}} |
| Black | {{sb=53}} |
| White | {{sw=47}} |
| Portion of the Black vote each candidate receives | {{b1+b2+b3 == 1}} |
| Barnes | {{b1=0}} |
| Foote | {{b2=0}} |
| Greene | {{b3=0}} |
| Portion of the white vote each candidate receives | {{w1+w2+w3 == 1}} |
| Barnes | {{w1=0}} |
| Foote | {{w2=0}} |
| Greene | {{w3=0}} |
| Final | |
| Barnes | {{sb*b1 + sw*w1}} |
| Foote | {{sb*b2 + sw*w2}} |
| Greene | {{sb*b3 + sw*w3}} |
Your projection totals for the portion of the vote each candidate receives must add up to 100%. In Ward 7, experts say Black voters could make up between 40-60% of the turnout.
Ward 7 council race: Kevin Parkinson (D), Taylor Turcotte (R), Ron Aldridge (I)
| Base Assumptions | You decide |
|---|---|
| Portion of the vote cast by each race | {{sb+sw == 1}} |
| Black | {{sb=54}} |
| White | {{sw=46}} |
| Portion of the Black vote each candidate receives | {{b1+b2+b3 == 1}} |
| Parkinson | {{b1=0}} |
| Turcotte | {{b2=0}} |
| Aldridge | {{b3=0}} |
| Portion of the white vote each candidate receives | {{w1+w2+w3 == 1}} |
| Parkinson | {{w1=0}} |
| Turcotte | {{w2=0}} |
| Aldridge | {{w3=0}} |
| Final | |
| Parkinson | {{sb*b1 + sw*w1}} |
| Turcotte | {{sb*b2 + sw*w2}} |
| Aldridge | {{sb*b3 + sw*w3}} |
tables.forEach((table, tableIndex) => { // Check if table contains {{variables}} const hasVariables = table.innerHTML.includes('{{'); if (!hasVariables) return;
// Create calculator instance for this table const calculator = new TableCalculator(table, tableIndex); calculator.initialize(); }); }
class TableCalculator { constructor(table, tableIndex) { this.table = table; this.tableIndex = tableIndex; this.variables = {}; this.validations = {}; this.formulas = {}; this.cells = new Map(); }
initialize() { this.parseTable(); this.createInputs(); this.setDefaultValues(); this.calculate(); }
parseTable() { const cells = this.table.querySelectorAll('td, th');
cells.forEach(cell => { const content = cell.textContent.trim(); const match = content.match(/\{\{(.+?)\}\}/);
if (match) { const expression = match[1].trim(); const cellId = `table${this.tableIndex}_${Array.from(cells).indexOf(cell)}`;
// Check if it's a validation (contains ==) if (expression.includes('==')) { const [left, right] = expression.split('==').map(s => s.trim()); this.validations[cellId] = { cell: cell, left: left, right: right, expression: expression }; } // Check if it's a default value (single =) else if (expression.includes('=') && !expression.includes('*') && !expression.includes('+') && !expression.includes('-') && !expression.includes('/')) { const [varName, defaultValue] = expression.split('=').map(s => s.trim()); this.variables[varName] = { cell: cell, defaultValue: parseFloat(defaultValue), cellId: cellId }; } // Otherwise it's a formula else { this.formulas[cellId] = { cell: cell, expression: expression }; }
this.cells.set(cellId, cell); } }); }
createInputs() { // Create inputs for variables Object.entries(this.variables).forEach(([varName, varData]) => { const input = document.createElement('input'); input.type = 'number'; input.min = '0'; input.step = '1'; input.value = varData.defaultValue; input.style.cssText = ` width: 60px; text-align: center; border: 1px solid #007cba; border-radius: 4px; padding: 0; font-weight: bold; background-color: #f0f8ff; `;
input.addEventListener('input', () => { this.handleBalancedInputs(varName, input); this.calculate(); });
// Replace cell content with input + % sign const container = document.createElement('div'); container.style.cssText = 'display: flex; align-items: center; justify-content: center; gap: 2px;'; container.appendChild(input); container.appendChild(document.createTextNode('%'));
varData.cell.innerHTML = ''; varData.cell.appendChild(container); varData.input = input; });
// Prepare validation cells Object.values(this.validations).forEach(validation => { validation.cell.innerHTML = ''; validation.cell.style.padding = '8px'; });
// Prepare formula cells Object.values(this.formulas).forEach(formula => { formula.cell.style.cssText += 'font-weight: bold; background-color: #f9f9f9;'; }); }
handleBalancedInputs(changedVarName, changedInput) { // Define balanced pairs - add more pairs as needed const balancedPairs = [ ['sb', 'sw'], ['sw', 'sb'] ];
// Find if this variable is part of a balanced pair const pair = balancedPairs.find(p => p[0] === changedVarName); if (!pair) return;
const partnerVarName = pair[1]; const partnerVar = this.variables[partnerVarName];
if (partnerVar && partnerVar.input) { const changedValue = parseFloat(changedInput.value) || 0; const newPartnerValue = 100 - changedValue;
// Ensure the partner value stays within bounds if (newPartnerValue >= 0 && newPartnerValue <= 100) { partnerVar.input.value = newPartnerValue; } else { // If partner would go out of bounds, adjust the changed value instead const maxChangedValue = changedValue > 100 ? 100 : (changedValue < 0 ? 0 : changedValue); changedInput.value = maxChangedValue; partnerVar.input.value = 100 - maxChangedValue; } } } setDefaultValues() { Object.entries(this.variables).forEach(([varName, varData]) => { if (varData.input) { varData.input.value = varData.defaultValue; } }); }
getVariableValue(varName) { const varData = this.variables[varName]; if (!varData || !varData.input) return 0;
const value = parseFloat(varData.input.value) || 0; return value / 100; // Convert percentage to decimal for calculations }
evaluateExpression(expression) { try { // Replace variable names with their values let evalExpression = expression; Object.keys(this.variables).forEach(varName => { const value = this.getVariableValue(varName); evalExpression = evalExpression.replace(new RegExp(`\\b${varName}\\b`, 'g'), value.toString()); });
// Safely evaluate the expression const result = Function(`"use strict"; return (${evalExpression})`)(); return result; } catch (error) { console.error('Error evaluating expression:', expression, error); return 0; } }
calculate() { let hasValidationErrors = false;
// Clear previous error highlighting this.table.querySelectorAll('.validation-error').forEach(cell => { cell.classList.remove('validation-error'); cell.style.backgroundColor = ''; });
// Check validations first Object.values(this.validations).forEach(validation => { const leftValue = this.evaluateExpression(validation.left); const rightValue = this.evaluateExpression(validation.right);
const isValid = Math.abs(leftValue - rightValue) < 0.001; // Allow small floating point differences if (!isValid) { hasValidationErrors = true; validation.cell.innerHTML = `Must add up to 100%.`;
// Highlight all variables involved in this validation const allVars = [...validation.left.matchAll(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g), ...validation.right.matchAll(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g)];
allVars.forEach(match => { const varName = match[0]; if (this.variables[varName]) { this.variables[varName].cell.style.backgroundColor = 'rgba(255, 0, 0, 0.2)'; this.variables[varName].cell.classList.add('validation-error'); } }); } else { validation.cell.innerHTML = ''; } });
// Calculate formulas only if no validation errors if (!hasValidationErrors) { Object.values(this.formulas).forEach(formula => { const result = this.evaluateExpression(formula.expression); const percentage = Math.round(result * 100); // Convert back to percentage and round formula.cell.innerHTML = `${percentage}%`; }); } else { // Clear formula cells if validation fails Object.values(this.formulas).forEach(formula => { formula.cell.innerHTML = ''; }); } } }
// Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeTableCalculators); } else { initializeTableCalculators(); }