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
// Plot 1: Mean Beak Length Comparison (with SEM)
{
// Calculate mean and SEM per condition
const meanData = d3.rollup(
birds,
v => ({
mean: d3.mean(v, d => d.bill_length),
stderr: d3.deviation(v, d => d.bill_length) / Math.sqrt(v.length)
}),
d => d.condition
);

const conditions = Array.from(meanData.keys());
// Set up SVG
const svg = d3.create("svg")
.attr("width", 500)
.attr("height", 300)
.style("background", "#f8f9fa");

const margin = { top: 30, right: 30, bottom: 50, left: 50 };
const width = 500 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;

// Scales
const x = d3.scaleBand()
.domain(conditions)
.range([0, width])
.padding(0.2);

const y = d3.scaleLinear()
.domain([0, d3.max(Array.from(meanData.values()), d => d.mean + d.stderr) * 1.1])
.range([height, 0]);

// Main plot area
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);

// Bars
g.selectAll(".bar")
.data(conditions)
.join("rect")
.attr("class", "bar")
.attr("x", d => x(d))
.attr("y", d => y(meanData.get(d).mean))
.attr("width", x.bandwidth())
.attr("height", d => height - y(meanData.get(d).mean))
.attr("fill", (d, i) => d3.schemeTableau10[i]);

// Error bars (SEM)
g.selectAll(".error-bar")
.data(conditions)
.join("line")
.attr("class", "error-bar")
.attr("x1", d => x(d) + x.bandwidth() / 2)
.attr("x2", d => x(d) + x.bandwidth() / 2)
.attr("y1", d => y(meanData.get(d).mean - meanData.get(d).stderr))
.attr("y2", d => y(meanData.get(d).mean + meanData.get(d).stderr))
.attr("stroke", "#333")
.attr("stroke-width", 1.5);

// Axes
g.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x))
.append("text")
.attr("class", "axis-label")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.text("Condition");

g.append("g")
.call(d3.axisLeft(y))
.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("y", -40)
.attr("x", -height / 2)
.attr("text-anchor", "middle")
.text("Beak Length (mm)");

// Title
g.append("text")
.attr("x", width / 2)
.attr("y", -10)
.attr("text-anchor", "middle")
.style("font-weight", "bold")
.text("Mean Beak Length (± SEM)");

return svg.node();
}
Insert cell
// Plot 2: Box Plot of Beak Length Distribution
{
const grouped = d3.group(birds, d => d.condition);
const conditions = Array.from(grouped.keys());

// Set up SVG
const svg = d3.create("svg")
.attr("width", 500)
.attr("height", 300)
.style("background", "#f8f9fa");

const margin = { top: 30, right: 30, bottom: 50, left: 50 };
const width = 500 - margin.left - margin.right;
const height = 300 - margin.top - margin.bottom;

// Scales
const x = d3.scaleBand()
.domain(conditions)
.range([0, width])
.padding(0.2);

const y = d3.scaleLinear()
.domain([d3.min(birds, d => d.bill_length) * 0.9, d3.max(birds, d => d.bill_length) * 1.05])
.range([height, 0]);

// Main plot area
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);

// Draw box plots for each condition
conditions.forEach((cond, i) => {
const values = grouped.get(cond).map(d => d.bill_length).sort(d3.ascending);
const q1 = d3.quantile(values, 0.25);
const median = d3.quantile(values, 0.5);
const q3 = d3.quantile(values, 0.75);
const iqr = q3 - q1;
const min = Math.max(q1 - 1.5 * iqr, d3.min(values));
const max = Math.min(q3 + 1.5 * iqr, d3.max(values));

const xPos = x(cond) + x.bandwidth() / 2;

// Box (IQR)
g.append("rect")
.attr("x", x(cond))
.attr("y", y(q3))
.attr("width", x.bandwidth())
.attr("height", y(q1) - y(q3))
.attr("fill", d3.schemeTableau10[i])
.attr("opacity", 0.3)
.attr("stroke", "#333")
.attr("stroke-width", 1.5);

// Median line
g.append("line")
.attr("x1", x(cond))
.attr("x2", x(cond) + x.bandwidth())
.attr("y1", y(median))
.attr("y2", y(median))
.attr("stroke", "red")
.attr("stroke-width", 2);

// Whiskers (min to Q1, Q3 to max)
g.append("line")
.attr("x1", xPos)
.attr("x2", xPos)
.attr("y1", y(min))
.attr("y2", y(q1))
.attr("stroke", "#333")
.attr("stroke-width", 1.5);

g.append("line")
.attr("x1", xPos)
.attr("x2", xPos)
.attr("y1", y(q3))
.attr("y2", y(max))
.attr("stroke", "#333")
.attr("stroke-width", 1.5);

// Whisker caps
g.append("line")
.attr("x1", xPos - 10)
.attr("x2", xPos + 10)
.attr("y1", y(min))
.attr("y2", y(min))
.attr("stroke", "#333")
.attr("stroke-width", 1.5);

g.append("line")
.attr("x1", xPos - 10)
.attr("x2", xPos + 10)
.attr("y1", y(max))
.attr("y2", y(max))
.attr("stroke", "#333")
.attr("stroke-width", 1.5);
});

// Axes
g.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x))
.append("text")
.attr("class", "axis-label")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.text("Condition");

g.append("g")
.call(d3.axisLeft(y))
.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("y", -40)
.attr("x", -height / 2)
.attr("text-anchor", "middle")
.text("Beak Length (mm)");

// Title
g.append("text")
.attr("x", width / 2)
.attr("y", -10)
.attr("text-anchor", "middle")
.style("font-weight", "bold")
.text("Beak Length Distribution (Box Plot)");

return svg.node();
}
Insert cell
Insert cell
Insert cell
// put your chart code or image here
{
const conditions = Array.from(new Set(birds.map(d => d.condition)));
const colorScale = d3.scaleOrdinal(d3.schemeTableau10).domain(conditions);
// Set up SVG
const svg = d3.create("svg")
.attr("width", 800)
.attr("height", 600)
.style("background", "#f8f9fa");

const margin = { top: 100, right: 100, bottom: -40, left: 70 };
const width = 800 - margin.left - margin.right;
const height = 500 - margin.top - margin.bottom;

// Scales
const x = d3.scaleLinear()
.domain([d3.min(birds, d => d.bill_length) * 0.95, d3.max(birds, d => d.bill_length) * 1.05])
.range([0, width]);

const y = d3.scaleLinear()
.domain([0, 0.06]) // Adjust based on density values
.range([height, 0]);

// Main plot area
const g = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);

// Histogram
const histogram = d3.bin()
.value(d => d.bill_length)
.domain(x.domain())
.thresholds(15);

conditions.forEach(cond => {
const data = birds.filter(d => d.condition === cond);
const bins = histogram(data);
// Calculate max density for scaling
const maxDensity = d3.max(bins, d => d.length / data.length / (d.x1 - d.x0));
// Scale histogram to match KDE range
const scaleFactor = 0.06 / maxDensity * 0.8; // 80% of plot height
g.selectAll(`.histogram-${cond}`)
.data(bins)
.join("rect")
.attr("x", d => x(d.x0) + 1)
.attr("y", d => y(scaleFactor * (d.length / data.length / (d.x1 - d.x0))))
.attr("width", d => Math.max(0, x(d.x1) - x(d.x0) - 1))
.attr("height", d => height - y(scaleFactor * (d.length / data.length / (d.x1 - d.x0))))
.attr("fill", colorScale(cond))
.attr("opacity", 0.3);
});

// Raw Data Points
conditions.forEach(cond => {
g.selectAll(`.point-${cond}`)
.data(birds.filter(d => d.condition === cond))
.join("circle")
.attr("cx", d => x(d.bill_length) + (Math.random() - 0.5) * 5) // Jitter
.attr("cy", d => y(0.01) + Math.random() * 20) // Offset from baseline
.attr("r", 2)
.attr("fill", colorScale(cond))
.attr("opacity", 0.6);
});

// X-axis
g.append("g")
.attr("transform", `translate(0,${height})`)
.call(d3.axisBottom(x))
.append("text")
.attr("class", "axis-label")
.attr("x", width / 2)
.attr("y", 45)
.attr("fill", "currentColor")
.style("font-size", "14px")
.style("text-anchor", "middle")
.text("Beak Length (mm)");

// Y-axis
g.append("g")
.call(d3.axisLeft(y).tickFormat(d3.format(".0%")))
.append("text")
.attr("class", "axis-label")
.attr("transform", "rotate(-90)")
.attr("y", -50)
.attr("x", -height / 2)
.attr("fill", "currentColor")
.style("font-size", "14px")
.style("text-anchor", "middle")
.text("Density");


// Legend
const legend = g.append("g")
.attr("transform", `translate(${width - 100}, 20)`);

conditions.forEach((cond, i) => {
legend.append("rect")
.attr("x", 0)
.attr("y", i * 20)
.attr("width", 15)
.attr("height", 15)
.attr("fill", colorScale(cond))
.attr("opacity", 0.6);

legend.append("text")
.attr("x", 20)
.attr("y", i * 20 + 12)
.text(cond)
.style("font-size", "12px");
});

// Title
g.append("text")
.attr("x", width / 2)
.attr("y", -10)
.attr("text-anchor", "middle")
.style("font-weight", "bold")
.text("Beak Length Distribution by Condition");

return svg.node();
}
Insert cell
Insert cell
Insert cell
render({
data: { values: birds },
layer: [
// Scatter plot points
{
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 } }
}
},
{
mark: { type: 'line' },
transform: [
{ regression: 'bill_depth', on: 'bill_length', groupby: ['condition'] }
],
encoding: {
x: { field: 'bill_length', type: 'Q' },
y: { field: 'bill_depth', type: 'Q' },
color: { field: 'condition', type: 'N' }
}
}
]
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// write your code here
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