Public
Edited
May 25
Insert cell
Insert cell
weeklyThickness = ({
"NFL": [23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 23.0, 31.1, 35.7, 40.4, 45.0],
"FBS": [16.4, 16.4, 16.4, 16.4, 16.4, 16.4, 16.4, 16.4, 16.4, 16.4, 16.4, 16.4, 16.4, 25.2, 31.5, 37.7, 45.0],
"NBA": [18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 18.9, 27.7, 30.5, 33.2, 35.9, 39.5, 42.3, 45.0],
"PGA": [25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 45.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 45.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 45.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 25.0, 45.0, 25.0, 25.0],
"MLB": [24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 24.0, 30.0, 34.0, 37.0, 41.0, 45.0],
"NHL": [19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 19.4, 29.0, 33.8, 37.0, 40.2, 45.0],
"EPL": [45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0, 45.0],
"CBB": [7.4, 7.4, 7.4, 7.4, 7.4, 7.4, 7.4, 7.4, 7.4, 7.4, 7.4, 7.4, 7.4, 7.4, 20.8, 29.2, 37.6, 45.0]
})

Insert cell
seasons = [
{ name: "NFL", sport: "NFL", start: "2024-09-05", end: "2025-02-09", color: "#00aaff" },
{ name: "College Football", sport: "FBS", start: "2024-08-27", end: "2025-01-06", color: "#ffaa00" },
{ name: "NBA", sport: "NBA", start: "2024-10-22", end: "2025-06-08", color: "#ff0033" },
{ name: "MLB", sport: "MLB", start: "2024-03-28", end: "2024-10-02", color: "#3399ff" },
{ name: "College Basketball", sport: "CBB", start: "2024-11-05", end: "2025-04-07", color: "#ff33cc" },
{ name: "Premier League", sport: "EPL", start: "2024-08-17", end: "2025-05-25", color: "#cc00ff" },
{ name: "NHL", sport: "NHL", start: "2024-10-08", end: "2025-06-15", color: "#66ff00" },
{ name: "PGA Tour", sport: "PGA", start: "2024-01-04", end: "2024-08-25", color: "#00ffcc" }
]
Insert cell
{
// Auto-fix weeklyThickness to match each season's actual number of weeks
for (let season of seasons) {
const { sport, start, end } = season;

const startDate = parseDate(start);
const endDate = parseDate(end);
if (!startDate || !endDate) {
console.warn(`Invalid dates for ${sport}:`, start, end);
continue;
}

const expectedWeeks = d3.utcWeek.count(startDate, endDate) + 1;

if (!weeklyThickness[sport]) {
console.warn(`Missing weeklyThickness for ${sport}`);
continue;
}

const currentWeeks = weeklyThickness[sport].length;

if (currentWeeks < expectedWeeks) {
const lastVal = weeklyThickness[sport][currentWeeks - 1];
const padAmount = expectedWeeks - currentWeeks;
for (let i = 0; i < padAmount; i++) {
weeklyThickness[sport].push(lastVal);
}
console.log(`✅ Padded ${sport}: ${currentWeeks} → ${expectedWeeks}`);
} else if (currentWeeks > expectedWeeks) {
weeklyThickness[sport] = weeklyThickness[sport].slice(0, expectedWeeks);
console.log(`✅ Trimmed ${sport}: ${currentWeeks} → ${expectedWeeks}`);
} else {
console.log(`✅ ${sport} is already correct: ${currentWeeks} weeks`);
}
}
}


Insert cell
parseDate = d3.timeParse("%Y-%m-%d")
Insert cell
formatMonth = d3.timeFormat("%b")
Insert cell
weeksInYear = 52
Insert cell
anglePerWeek = (2 * Math.PI) / weeksInYear
Insert cell
viewof radialChart = {
const svg = d3.create("svg")
.attr("width", 1000)
.attr("height", 1000)
.attr("viewBox", [-500, -500, 1000, 1000])
.style("font", "14px sans-serif");

const g = svg.append("g");

const centerHole = 80;
const monthRingOuter = centerHole + 30;

// Month grid + labels
for (let i = 0; i < 12; i++) {
const angleStart = (i / 12) * 2 * Math.PI;
const angleEnd = ((i + 1) / 12) * 2 * Math.PI;
const midAngle = (angleStart + angleEnd) / 2;

g.append("path")
.attr("d", d3.arc()({
innerRadius: centerHole,
outerRadius: monthRingOuter,
startAngle: angleStart,
endAngle: angleEnd
}))
.attr("fill", i % 2 === 0 ? "#fff" : "#eee");

g.append("text")
.attr("transform", `rotate(${(midAngle * 180 / Math.PI - 90)}) translate(${(centerHole + monthRingOuter) / 2},0)`)
.attr("dy", "0.35em")
.style("text-anchor", "middle")
.style("fill", "#000")
.text(formatMonth(new Date(2024, i, 1)));
}

const weeklyStacks = Array.from({ length: weeksInYear }, () => []);
for (const season of seasons) {
const { sport, color, start, end } = season;
const weeks = weeklyThickness[sport];
if (!weeks) continue;

const startDate = parseDate(start);
const startWeek = d3.utcWeek.count(new Date("2024-01-01"), startDate);
for (let j = 0; j < weeks.length; j++) {
const week = startWeek + j;
if (weeklyStacks[week]) {
weeklyStacks[week].push({
sport,
thickness: weeks[j],
color
});
}
}
}

let currentRadius = monthRingOuter;
for (let week = 0; week < weeksInYear; week++) {
const arcs = weeklyStacks[week];
if (!arcs || arcs.length === 0) continue;

arcs.sort((a, b) => b.thickness - a.thickness);
let inner = currentRadius;
for (const arc of arcs) {
const outer = inner + arc.thickness;
const angleStart = week * anglePerWeek;
const angleEnd = (week + 1) * anglePerWeek;

g.append("path")
.attr("d", d3.arc()({
innerRadius: inner,
outerRadius: outer,
startAngle: angleStart,
endAngle: angleEnd
}))
.attr("fill", arc.color)
.attr("stroke", "#fff");

inner = outer;
}
currentRadius = Math.max(currentRadius, inner);
}

// Labels stacked vertically
const labels = seasons.map(s => s.sport);
const labelSpacing = 25;
for (let i = 0; i < labels.length; i++) {
g.append("text")
.attr("x", 0)
.attr("y", -centerHole - 40 - i * labelSpacing)
.attr("text-anchor", "middle")
.style("font-size", "14px")
.style("font-weight", "bold")
.text(labels[i]);
}

// Grid lines aligned with months
const gridGroup = svg.insert("g", "g");
for (let i = 0; i < 12; i++) {
const angle = (i / 12) * 2 * Math.PI;
gridGroup.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", Math.cos(angle) * currentRadius)
.attr("y2", Math.sin(angle) * currentRadius)
.attr("stroke", "#000");
}

return svg.node();
}

Insert cell
seasons.map(s => s.sport).filter(s => !weeklyThickness[s])
Insert cell
seasons.forEach(season => {
const sport = season.sport;
const start = parseDate(season.start);
const end = parseDate(season.end);
const weeks = weeklyThickness[sport];

console.log(`${sport}: start = ${start}, end = ${end}, weeks = ${weeks?.length}`);
});


Insert cell
// Auto-fix weeklyThickness to match each season's actual number of weeks
for (const season of seasons) {
const { sport, start, end } = season;

const startDate = parseDate(start);
const endDate = parseDate(end);
if (!startDate || !endDate) {
console.warn(`Invalid dates for ${sport}:`, start, end);
continue;
}

const expectedWeeks = d3.utcWeek.count(startDate, endDate) + 1;

if (!weeklyThickness[sport]) {
console.warn(`Missing weeklyThickness for ${sport}`);
continue;
}

const currentWeeks = weeklyThickness[sport].length;

if (currentWeeks < expectedWeeks) {
const lastVal = weeklyThickness[sport][currentWeeks - 1];
const padAmount = expectedWeeks - currentWeeks;
for (let i = 0; i < padAmount; i++) {
weeklyThickness[sport].push(lastVal);
}
console.log(`✅ Padded ${sport}: ${currentWeeks} → ${expectedWeeks}`);
} else if (currentWeeks > expectedWeeks) {
weeklyThickness[sport] = weeklyThickness[sport].slice(0, expectedWeeks);
console.log(`✅ Trimmed ${sport}: ${currentWeeks} → ${expectedWeeks}`);
} else {
console.log(`✅ ${sport} is already correct: ${currentWeeks} weeks`);
}
}

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