${provinceName}
${currentElectionType === 'district' ? 'แบบแบ่งเขต' : 'แบบบัญชีรายชื่อ'}
เขต ${districtNo}
อันดับ 1: ${winnerParty}
ความผิดปกติ: ${anomalyStatus}
คะแนนชนะขาด: ${marginStatus}
`);
})
.on('mousemove', (event) => {
tooltip
.style('top', event.pageY - 60 + 'px') // ✨ ขยับขึ้นนิดนึงไม่ให้บังเมาส์
.style('left', event.pageX + 15 + 'px');
})
.on('mouseout', function (event, dot) {
// ✨ Animation หดกลับแบบนุ่มนวล
d3.select(this)
.transition().duration(200)
.attr('width', rectSize)
.attr('height', rectSize)
.attr('x', dot.pos[0] * dotGap - rectSize / 2)
.attr('y', dot.pos[1] * dotGap - rectSize / 2)
.attr('stroke', '#ffffff')
.attr('stroke-width', 0.5)
.attr('fill', () => {
if (currentParty !== 'ทั้งหมด') {
if (dot.data['เขตอันดับ 1'] !== currentParty)
return '#f0f0f0';
}
const yMap = { None: 0, Minor: 1, Major: 2 };
const xMap = {
Marginal: 0,
Moderate: 1,
'Safe-seat': 2,
};
const anomalyCol =
currentElectionType === 'district'
? 'ความผิดปกติ(เขต)'
: 'ความผิดปกติ(ปาร์ตี้ลิส)';
const marginCol =
currentElectionType === 'district'
? 'คะแนนชนะขาด(เขต)'
: 'คะแนนชนะขาด(ปาร์ตี้ลิส)';
const yIdx = yMap[dot.data[anomalyCol]] ?? 0;
const xIdx = xMap[dot.data[marginCol]] ?? 0;
return bivariateColors[yIdx][xIdx];
});
tooltip.style('visibility', 'hidden');
});
});
// --- ✨ สร้างตารางคำอธิบายสี (Bivariate Legend 3x3) วางไว้มุมขวาล่าง ---
const legendSize = 30;
const legendGroup = svg.append('g')
.attr('transform', translate(${width - 250}, ${height - 200})); // ใช้ Backticks (`) ให้ถูกต้อง
for (let y = 0; y < 3; y++) {
for (let x = 0; x < 3; x++) {
legendGroup.append('rect')
.attr('x', x * legendSize)
.attr('y', (2 - y) * legendSize) // สลับ Y ให้ "ปัญหามาก" อยู่บนสุด
.attr('width', legendSize)
.attr('height', legendSize)
.attr('fill', bivariateColors[y][x])
.attr('stroke', '#ffffff')
.attr('stroke-width', 1);
}
}
// แกน X (คะแนนชนะขาด)
legendGroup.append('text')
.attr('x', 45)
.attr('y', 110)
.attr('text-anchor', 'middle')
.style('font-size', '12px')
.style('fill', '#333')
.style('font-weight', 'bold')
.text('คะแนนชนะขาด ➔');
// แกน Y (ความผิดปกติ)
legendGroup.append('text')
.attr('x', -10)
.attr('y', 45)
.attr('transform', 'rotate(-90, -10, 45)')
.attr('text-anchor', 'middle')
.style('font-size', '12px')
.style('fill', '#333')
.style('font-weight', 'bold')
.text('ความผิดปกติ ➔');
// Label เล็กๆ บอกมุม
legendGroup.append('text').attr('x', 0).attr('y', 125).style('font-size', '10px').style('fill', '#666').text('สูสี');
legendGroup.append('text').attr('x', 70).attr('y', 125).style('font-size', '10px').style('fill', '#666').text('ชนะขาด');
legendGroup.append('text').attr('x', -25).attr('y', 85).style('font-size', '10px').style('fill', '#666').text('น้อย');
legendGroup.append('text').attr('x', -25).attr('y', 10).style('font-size', '10px').style('fill', '#666').text('มาก');
}
// ===============================
// EVENT LISTENERS
// ===============================
// ฟังการเปลี่ยนประเภทการเลือกตั้ง (Radio buttons)
d3.selectAll('input[name="electionType"]').on(
'change',
function () {
currentElectionType = this.value;
const data = summaryData[currentElectionType];
d3.select('#stat-number').text(data.total);
d3.select('#stat-percent').text(data.percent);
d3.select('#stat-note').text(data.note);
renderDashboard();
},
);
d3.selectAll('.tab-btn').on('click', function () {
d3.selectAll('.tab-btn').classed('active', false);
d3.select(this).classed('active', true);
const btnText = d3.select(this).text().trim();
if (btnText === 'เขตที่มีปัญหา') currentMode = 'problem';
else if (btnText === 'บัตรเสีย') currentMode = 'invalid';
else if (btnText === 'ยอดผู้มาใช้สิทธิ')
currentMode = 'user';
renderDashboard();
});
// ===============================
// LOAD DATA & INITIALIZE
// ===============================
async function init() {
try {
const csvData = await d3.csv('MainPointComplete.csv');
const dataMap = new Map(
csvData.map((d) => [d.จังหวัด_เขต, d]),
);
provinces = rawData.map((p) => {
return {
...p,
dots: p.dots.map((d, index) => {
const districtNumber = index + 1;
// ✨ แก้ไข Backticks (`) ที่หายไปให้เชื่อม string ถูกต้อง
const lookupKey = ${p.name}_${districtNumber};
const rowData = dataMap.get(lookupKey) || {};
return {
districtNo: districtNumber,
pos: d,
data: rowData,
};
}),
};
});
renderDashboard();
} catch (error) {
console.error('Error loading the CSV file:', error);
}
}
init();