uncertainty_chart = {
const margin = { top: 60, right: 120, bottom: 60, left: 120 };
const width = 600 - margin.left - margin.right;
const height = 400 - margin.top - margin.bottom;
const svg = d3.create("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom);
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const data = election_data.candidates.map(d => {
const currentPercent = d.votes / election_data.totalCounted * 100;
const remainingVotes = election_data.totalExpected - election_data.totalCounted;
const bestCase = (d.votes + remainingVotes) / election_data.totalExpected * 100;
const worstCase = d.votes / election_data.totalExpected * 100;
return {
...d,
currentPercent,
bestCase,
worstCase,
uncertainty: bestCase - worstCase
};
});
const y = d3.scaleBand()
.domain(data.map(d => d.name))
.range([0, height])
.padding(0.3);
const x = d3.scaleLinear()
.domain([0, 60])
.range([0, width]);
// 绘制不确定性区间(浅色背景)
g.selectAll(".uncertainty-bar")
.data(data)
.join("rect")
.attr("class", "uncertainty-bar")
.attr("x", d => x(d.worstCase))
.attr("y", d => y(d.name))
.attr("width", d => x(d.bestCase) - x(d.worstCase))
.attr("height", y.bandwidth())
.attr("fill", d => d.color)
.attr("opacity", 0.3)
.attr("stroke", d => d.color)
.attr("stroke-width", 1);
// 绘制当前得票率(深色条)
g.selectAll(".current-bar")
.data(data)
.join("rect")
.attr("class", "current-bar")
.attr("x", 0)
.attr("y", d => y(d.name))
.attr("width", d => x(d.currentPercent))
.attr("height", y.bandwidth())
.attr("fill", d => d.color)
.attr("opacity", 0.8);
// 添加50%获胜线
g.append("line")
.attr("x1", x(50))
.attr("x2", x(50))
.attr("y1", 0)
.attr("y2", height)
.attr("stroke", "red")
.attr("stroke-width", 2)
.attr("stroke-dasharray", "5,5");
g.append("text")
.attr("x", x(50))
.attr("y", -10)
.attr("text-anchor", "middle")
.attr("fill", "red")
.attr("font-weight", "bold")
.text("50% (获胜线)");
// 坐标轴
g.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x).tickFormat(d => d + "%"));
g.append("g")
.call(d3.axisLeft(y));
// 标题
svg.append("text")
.attr("x", (width + margin.left + margin.right) / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.attr("font-size", "16px")
.attr("font-weight", "bold")
.text("迪士尼乐园市长选举 - 不确定性分析");
svg.append("text")
.attr("x", (width + margin.left + margin.right) / 2)
.attr("y", 45)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.attr("fill", "gray")
.text(`已统计 ${election_data.percentCounted}% 选票`);
// 图例
const legend = svg.append("g")
.attr("transform", `translate(${width + margin.left + 20}, ${margin.top})`);
legend.append("rect")
.attr("width", 15)
.attr("height", 15)
.attr("fill", "gray")
.attr("opacity", 0.8);
legend.append("text")
.attr("x", 20)
.attr("y", 12)
.attr("font-size", "12px")
.text("当前得票率");
legend.append("rect")
.attr("y", 25)
.attr("width", 15)
.attr("height", 15)
.attr("fill", "gray")
.attr("opacity", 0.3)
.attr("stroke", "gray");
legend.append("text")
.attr("x", 20)
.attr("y", 37)
.attr("font-size", "12px")
.text("可能范围");
// 数据标签
g.selectAll(".current-label")
.data(data)
.join("text")
.attr("class", "current-label")
.attr("x", d => x(d.currentPercent) + 5)
.attr("y", d => y(d.name) + y.bandwidth()/2 + 4)
.attr("font-size", "12px")
.attr("font-weight", "bold")
.text(d => `${d.currentPercent.toFixed(1)}%`);
g.selectAll(".range-label")
.data(data)
.join("text")
.attr("class", "range-label")
.attr("x", d => x(d.bestCase) + 5)
.attr("y", d => y(d.name) + y.bandwidth()/2 + 4)
.attr("font-size", "10px")
.attr("fill", "gray")
.text(d => `(${d.worstCase.toFixed(1)}%-${d.bestCase.toFixed(1)}%)`);
return svg.node();
}