initChart()); flashRefresh()"
x-data="{
view: 'table',
chart: null,
refreshFlash: false,
previewOpen: false,
previewPhoto: '',
previewName: '',
openPreview(photo, name) {
this.previewPhoto = photo;
this.previewName = name;
this.previewOpen = true;
},
init() {
const saved = sessionStorage.getItem('resultsView');
if (saved) {
const allowed = ['table','chart','roster'];
if (allowed.includes(saved)) this.view = saved;
}
this.$nextTick(() => { this.initChart(); });
},
setView(v) {
this.view = v;
sessionStorage.setItem('resultsView', v);
if (v === 'chart') this.$nextTick(() => { this.initChart(); });
},
flashRefresh() {
// Brief highlight so an auto-refresh (every 30s) reads as an
// intentional live update rather than a silent, unnoticed change.
this.refreshFlash = true;
setTimeout(() => { this.refreshFlash = false; }, 900);
},
initChart() {
if (typeof Chart === 'undefined') return;
if (this.view !== 'chart') return;
const canvas = this.$refs.chartCanvas;
if (!canvas) return;
if (this.chart) { this.chart.destroy(); this.chart = null; }
// Read fresh vote data from a plain attribute (kept in sync by
// Livewire's normal morph on every poll) rather than a JS
// closure — the closure would freeze whatever data existed
// the moment Alpine first parsed x-data and never update,
// since Livewire patches this element's own attributes
// in place rather than re-initialising Alpine on it.
const payloadEl = this.$refs.chartPayload;
const data = payloadEl ? JSON.parse(payloadEl.dataset.chartPayload || '[]') : [];
// Same primary-600 → brand-green gradient as the table/roster progress
// bars, read straight from app.css so both stay in sync with one source.
const rootStyles = getComputedStyle(document.documentElement);
const primary600 = rootStyles.getPropertyValue('--primary-600').trim() || '#e8420d';
const brandGreen = rootStyles.getPropertyValue('--brand-green').trim() || '#059669';
const barGradient = (ctx) => {
const { chartArea } = ctx.chart;
if (!chartArea) return primary600;
const gradient = ctx.chart.ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top);
gradient.addColorStop(0, primary600);
gradient.addColorStop(1, brandGreen);
return gradient;
};
this.chart = new Chart(canvas.getContext('2d'), {
type: 'bar',
data: {
labels: data.map(n => n.name),
datasets: [{
label: 'Votes',
data: data.map(n => n.votes),
backgroundColor: barGradient,
borderColor: brandGreen,
borderWidth: 1,
borderRadius: 6,
barThickness: 36,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 500 },
scales: {
x: {
grid: { display: false },
ticks: {
font: { weight: '700' },
color: '#27272a',
maxRotation: 30,
minRotation: 0,
}
},
y: {
beginAtZero: true,
grid: { color: '#f4f4f5' },
ticks: { font: { weight: '600' }, color: '#a1a1aa' }
}
},
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#020238',
padding: 12,
titleFont: { size: 14, weight: '800' },
bodyFont: { size: 13, weight: '600' },
callbacks: {
label: (ctx) => {
const d = data[ctx.dataIndex];
return ` ${ctx.parsed.y.toLocaleString()} votes (${d.pct}%)`;
}
}
}
}
}
});
}
}">
Standings
4 nominees
| Rank |
Nominee |
Votes |
Share |
|
1
|
Abigail Owusu Ansah
PUDAN66
|
1,000
|
|
|
2
|
|
270
|
|
|
3
|
|
225
|
|
|
4
|
|
22
|
|
1,517 total votes cast
Aug 05, 2026 · 06:23 AM
Vote Distribution
1,517 total votes
Aug 05, 2026 · 06:23 AM