Public
Edited
Apr 24
Insert cell
Insert cell
data = [
{ Team: "Team 1", "Projected Points": 88 },
{ Team: "Team 2", "Projected Points": 84 },
{ Team: "Team 3", "Projected Points": 82 },
{ Team: "Team 4", "Projected Points": 76 },
{ Team: "Team 5", "Projected Points": 70 },
{ Team: "Team 6", "Projected Points": 65 }
]
Insert cell
viewof chart = {
const margin = { top: 80, right: 40, bottom: 80, left: 160 }
const width = 800
const height = 440

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.style("background", "#fff")
.style("font-family", "'Helvetica Neue', Georgia, serif")
.style("font-size", "13px")

const chart = svg.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`)

const innerWidth = width - margin.left - margin.right
const innerHeight = height - margin.top - margin.bottom

// Scales
const x = d3.scaleLinear()
.domain([0, d3.max(data, d => d["Projected Points"])])
.nice()
.range([0, innerWidth])

const y = d3.scaleBand()
.domain(data.map(d => d.Team))
.range([0, innerHeight])
.padding(0.2)

// Gradient Definition
const defs = svg.append("defs")
const gradient = defs.append("linearGradient")
.attr("id", "bar-gradient")
.attr("x1", "0%").attr("x2", "100%")
.attr("y1", "0%").attr("y2", "0%")
gradient.append("stop")
.attr("offset", "0%")
.attr("stop-color", "#a6d9f7")
gradient.append("stop")
.attr("offset", "100%")
.attr("stop-color", "#005ea2")

// Gridlines
chart.append("g")
.call(d3.axisBottom(x)
.tickSize(innerHeight)
.tickFormat(""))
.attr("transform", `translate(0,0)`)
.selectAll("line")
.attr("stroke", "#eee")

// Axes
chart.append("g").call(d3.axisLeft(y))
chart.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(x).ticks(5))
.call(g => g.append("text")
.attr("x", innerWidth / 2)
.attr("y", 45)
.attr("fill", "#333")
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.text("Projected Points"))

// Bars
chart.selectAll("rect")
.data(data)
.join("rect")
.attr("x", 0)
.attr("y", d => y(d.Team))
.attr("width", d => x(d["Projected Points"]))
.attr("height", y.bandwidth())
.attr("fill", "url(#bar-gradient)")

// Labels
chart.selectAll("text.label")
.data(data)
.join("text")
.attr("class", "label")
.attr("x", d => x(d["Projected Points"]) + 6)
.attr("y", d => y(d.Team) + y.bandwidth() / 2)
.attr("dy", "0.35em")
.attr("fill", "#333")
.attr("font-weight", "bold")
.text(d => d["Projected Points"])

// Title
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "22px")
.attr("font-weight", "bold")
.text("Projected League Standings")

// Subtitle
svg.append("text")
.attr("x", width / 2)
.attr("y", 52)
.attr("text-anchor", "middle")
.attr("font-size", "14px")
.attr("fill", "#666")
.text("Based on my mode's strength estimates — higher point totals indicate stronger season outlooks")

// Source
svg.append("text")
.attr("x", width - 10)
.attr("y", height - 10)
.attr("text-anchor", "end")
.attr("font-size", "11px")
.attr("fill", "#666")
.text("Source: Marc Lamberts | Chart: @lambertsmarc.bsky.social")

return svg.node()
}

Insert cell
viewof boxplotChart = {
const margin = { top: 80, right: 40, bottom: 80, left: 100 };
const width = 700;
const height = 300;

const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.style("background", "#fff")
.style("font-family", "'Helvetica Neue', Georgia, serif")
.style("font-size", "13px");

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

const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;

// 🛠 Prepare clean SPI values
const spiValues = data
.map(d => +d.SPI)
.filter(d => !isNaN(d))
.sort(d3.ascending);

// 📈 Compute box plot stats
const q1 = d3.quantile(spiValues, 0.25);
const median = d3.quantile(spiValues, 0.5);
const q3 = d3.quantile(spiValues, 0.75);
const min = d3.min(spiValues);
const max = d3.max(spiValues);

const x = d3.scaleLinear()
.domain([min - 5, max + 5])
.range([0, innerWidth]);

// 📏 X Axis
chart.append("g")
.attr("transform", `translate(0,${innerHeight / 2 + 20})`)
.call(d3.axisBottom(x).ticks(6));

// 📦 Box (Q1 to Q3)
chart.append("rect")
.attr("x", x(q1))
.attr("y", innerHeight / 2 - 20)
.attr("width", x(q3) - x(q1))
.attr("height", 40)
.attr("fill", "#007BC7")
.attr("opacity", 0.2)
.attr("stroke", "#007BC7");

// ➖ Median line
chart.append("line")
.attr("x1", x(median))
.attr("x2", x(median))
.attr("y1", innerHeight / 2 - 20)
.attr("y2", innerHeight / 2 + 20)
.attr("stroke", "#007BC7")
.attr("stroke-width", 2);

// ── Whiskers
chart.append("line")
.attr("x1", x(min))
.attr("x2", x(q1))
.attr("y1", innerHeight / 2)
.attr("y2", innerHeight / 2)
.attr("stroke", "#aaa");

chart.append("line")
.attr("x1", x(q3))
.attr("x2", x(max))
.attr("y1", innerHeight / 2)
.attr("y2", innerHeight / 2)
.attr("stroke", "#aaa");

// ✏️ Min/Max markers
chart.selectAll("line.whisker")
.data([min, max])
.join("line")
.attr("x1", d => x(d))
.attr("x2", d => x(d))
.attr("y1", innerHeight / 2 - 10)
.attr("y2", innerHeight / 2 + 10)
.attr("stroke", "#aaa");

// 📍 Labels (safe for undefined)
chart.selectAll("text.stats")
.data([
{ label: "Min", value: min },
{ label: "Q1", value: q1 },
{ label: "Median", value: median },
{ label: "Q3", value: q3 },
{ label: "Max", value: max }
])
.join("text")
.attr("x", d => x(d.value))
.attr("y", innerHeight / 2 - 30)
.attr("text-anchor", "middle")
.attr("fill", "#333")
.text(d => `${d.label}: ${d.value != null ? d.value.toFixed(1) : "N/A"}`);

// 📰 Title
svg.append("text")
.attr("x", width / 2)
.attr("y", 30)
.attr("text-anchor", "middle")
.attr("font-size", "20px")
.attr("font-weight", "bold")
.text("SPI Distribution for Premier League Teams");

// 🧾 Subtitle
svg.append("text")
.attr("x", width / 2)
.attr("y", 50)
.attr("text-anchor", "middle")
.attr("font-size", "13px")
.attr("fill", "#666")
.text("Box plot of Soccer Power Index (SPI) from FiveThirtyEight predictions");

// 🔗 Source
svg.append("text")
.attr("x", width - 10)
.attr("y", height - 10)
.attr("text-anchor", "end")
.attr("font-size", "11px")
.attr("fill", "#666")
.text("Source: FiveThirtyEight | Chart: You");

return svg.node();
}

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