Public
Edited
May 22
Insert cell
Insert cell
Insert cell
Insert cell
birds = FileAttachment('birds.json').json();
Insert cell
Inputs.table(birds, { width: 500 })
Insert cell
Insert cell
Insert cell
summary_stats = {
const grouped = d3.group(birds, d => d.condition);
const stats = [];
for (let [condition, data] of grouped) {
const lengths = data.map(d => d.bill_length);
const mean = d3.mean(lengths);
const std = d3.deviation(lengths);
const se = std / Math.sqrt(lengths.length);
stats.push({
condition: condition,
mean: mean,
standardError: se,
count: lengths.length
});
}
return stats;
}
Insert cell
{
const width = 400;
const height = 300;
const margin = {top: 20, right: 20, bottom: 40, left: 50};
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const x = d3.scaleBand()
.domain(summary_stats.map(d => d.condition))
.range([margin.left, width - margin.right])
.padding(0.4);
const y = d3.scaleLinear()
.domain([0, d3.max(summary_stats, d => d.mean + d.standardError) * 1.1])
.range([height - margin.bottom, margin.top]);
// 柱状图
svg.selectAll("rect")
.data(summary_stats)
.join("rect")
.attr("x", d => x(d.condition))
.attr("y", d => y(d.mean))
.attr("width", x.bandwidth())
.attr("height", d => y(0) - y(d.mean))
.attr("fill", "steelblue");
// 误差条
svg.selectAll("line")
.data(summary_stats)
.join("line")
.attr("x1", d => x(d.condition) + x.bandwidth() / 2)
.attr("x2", d => x(d.condition) + x.bandwidth() / 2)
.attr("y1", d => y(d.mean - d.standardError))
.attr("y2", d => y(d.mean + d.standardError))
.attr("stroke", "red")
.attr("stroke-width", 2);
// 坐标轴
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x));
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y));
// 标题
svg.append("text")
.attr("x", width / 2)
.attr("y", 15)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.text("Mean Bill Length by Condition");
return svg.node();
}

Insert cell
Insert cell
Insert cell
// put your chart code or image here
{
const width = 400;
const height = 300;
const margin = {top: 20, right: 20, bottom: 40, left: 50};
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height);
const x = d3.scalePoint()
.domain([...new Set(birds.map(d => d.condition))].sort())
.range([margin.left + 20, width - margin.right - 20]);
const y = d3.scaleLinear()
.domain(d3.extent(birds, d => d.bill_length))
.nice()
.range([height - margin.bottom, margin.top]);
const color = d3.scaleOrdinal()
.domain([...new Set(birds.map(d => d.condition))])
.range(["orange", "steelblue"]);
// 为每个点添加随机偏移避免重叠
const jitteredData = birds.map(d => ({
...d,
jitter: (Math.random() - 0.5) * 20
}));
// 散点
svg.selectAll("circle")
.data(jitteredData)
.join("circle")
.attr("cx", d => x(d.condition) + d.jitter)
.attr("cy", d => y(d.bill_length))
.attr("r", 3)
.attr("fill", d => color(d.condition))
.attr("opacity", 0.7);
// 坐标轴
svg.append("g")
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x));
svg.append("g")
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y));
// 标题
svg.append("text")
.attr("x", width / 2)
.attr("y", 15)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.text("Distribution of Bill Length");
return svg.node();
}

Insert cell
{
const div = d3.create("div");
const table = div.append("table")
.style("border-collapse", "collapse")
.style("width", "100%")
.style("font-family", "Arial, sans-serif");
// 表头
const thead = table.append("thead");
thead.append("tr")
.selectAll("th")
.data(["Condition", "Mean", "Std Error", "Count"])
.join("th")
.style("border", "1px solid #ddd")
.style("padding", "8px")
.style("background-color", "#f2f2f2")
.text(d => d);
// 表格内容
const tbody = table.append("tbody");
tbody.selectAll("tr")
.data(summary_stats)
.join("tr")
.selectAll("td")
.data(d => [d.condition, d.mean.toFixed(2), d.standardError.toFixed(3), d.count])
.join("td")
.style("border", "1px solid #ddd")
.style("padding", "8px")
.style("text-align", "center")
.text(d => d);
return div.node();
}

Insert cell
Insert cell
Insert cell
render({
data: { values: birds },
layer: [
{
mark: 'circle',
encoding: {
x: { field: 'bill_length', type: 'Q', scale: { zero: false } },
y: { field: 'bill_depth', type: 'Q', scale: { zero: false } },
color: { field: 'condition', type: 'N' }
}
},
{
mark: { type: 'line', stroke: 'black' },
transform: [{ regression: 'bill_depth', on: 'bill_length' }],
encoding: {
x: { field: 'bill_length', type: 'Q', scale: { zero: false } },
y: { field: 'bill_depth', type: 'Q', scale: { zero: false } }
}
},
// add another mark layer here
]
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// 主要可视化:带不确定性区间的条形图
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();
}


Insert cell
// 辅助可视化:概率密度图(简化版)
probability_viz = {
const margin = { top: 40, right: 40, bottom: 60, left: 60 };
const width = 500 - margin.left - margin.right;
const height = 300 - 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 mickeyCurrentPercent = 18073 / 35920 * 100;
const scenarios = [];
for (let i = 0; i <= 100; i++) {
const remainingForMickey = i / 100;
const remainingVotes = 44900 - 35920;
const mickeyFinalVotes = 18073 + remainingVotes * remainingForMickey;
const mickeyFinalPercent = mickeyFinalVotes / 44900 * 100;
// 假设均匀分布(实际可能使用beta分布)
const probability = 1; // 简化为均匀概率
scenarios.push({ percent: mickeyFinalPercent, probability });
}
const x = d3.scaleLinear()
.domain([35, 65])
.range([0, width]);
const y = d3.scaleLinear()
.domain([0, 0.1])
.range([height, 0]);
// 绘制概率密度曲线
const line = d3.line()
.x(d => x(d.percent))
.y(d => y(0.05)) // 简化为常数高度
.curve(d3.curveCardinal);
g.append("path")
.datum(scenarios.filter(d => d.percent >= 35 && d.percent <= 65))
.attr("fill", "none")
.attr("stroke", "#4472C4")
.attr("stroke-width", 2)
.attr("d", line);
// 填充获胜区域
const winningArea = scenarios.filter(d => d.percent > 50);
if (winningArea.length > 0) {
g.append("path")
.datum(winningArea)
.attr("fill", "#4472C4")
.attr("fill-opacity", 0.3)
.attr("d", d3.area()
.x(d => x(d.percent))
.y0(height)
.y1(y(0.05))
.curve(d3.curveCardinal));
}
// 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", "3,3");
// 坐标轴
g.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x).tickFormat(d => d + "%"));
g.append("g")
.call(d3.axisLeft(y).ticks(3));
// 标题和标签
svg.append("text")
.attr("x", (width + margin.left + margin.right) / 2)
.attr("y", 25)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.text("Mickey Mouse 获胜概率分布");
g.append("text")
.attr("x", width / 2)
.attr("y", height + 45)
.attr("text-anchor", "middle")
.attr("font-size", "12px")
.text("最终得票率 (%)");
return svg.node();
}


Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more