mlb_stats = FileAttachment("data/mlb_stats.csv").csv({typed: true})
nba_stats = FileAttachment("data/nba_stats.csv").csv({typed: true})
nhl_stats = FileAttachment("data/nhl_stats.csv").csv({typed: true})
wnba_stats = FileAttachment("data/wnba_stats.csv").csv({typed: true})
f1_stats = FileAttachment("data/f1_stats.csv").csv({typed: true})
results = league==="wnba_stats" ? wnba_stats : league==="nba_stats" ? nba_stats : league==="mlb_stats" ? mlb_stats : league==="nhl_stats" ? nhl_stats : f1_stats
metrics = Object.keys(results[0]).slice(8).sort()
top_5 = results.sort((a, b) => b[metric] - a[metric]).slice(0,5);
pre_title = league==="nba_stats" ? "NBA" : league==="mlb_stats" ? "MLB" : league==="wnba_stats" ? "WNBA" : league==="nhl_stats" ? "NHL" : ""
default_stat = league==="nba_stats" ? "Points Per Game" : league==="mlb_stats" ? "Batting Average" : league==="wnba_stats" ? "Points Per Game" : league==="nhl_stats" ? "Shooting Percentage" : "Points"
Sports Leaderboard Generator
Observable
interactive
D3
sports
Dynamic Sports Leaderboard Generator. Stats and images taken from ESPN for leagues including NBA, MLB, NHL, WNBA, and F1.
Aesthetics
viewof box_color_type = {
const input = Inputs.radio(["Mapped","Uniform"], {label: "Box Color Type", value:"Mapped"});
input.classList.add("radio");
return input;
}
viewof box_color= Inputs.text({label: "Box Color", placeholder: "Box Color", value:'black', disabled: box_color_type==="Uniform"? false: true})
viewof box_stroke= Inputs.text({label: "Box Stroke", placeholder: "Box Color", value:'none'})
viewof background = Inputs.text({label: "Background", placeholder: "Background Color", value:'#1E252C'})
viewof font_family= Inputs.select(['Antonio','Archivo Narrow','Bebas Neue','BenchNine', "IBM Plex Sans Condensed", 'Jockey One','Oswald',"Roboto Condensed","Sofia Sans Condensed"], {label:"Font Family", value:"Oswald"})
viewof circle_color= Inputs.text({label: "Circle Color", placeholder: "Color", value:'#EEF0F2'})
viewof font_color= Inputs.text({label: "Font Color", placeholder: "Color", value:'white'})
viewof title_color= Inputs.text({label: "Title Color", placeholder: "Color", value:'white'})
viewof top_player_font_size= Inputs.range([0, 50], {value: 32, step: 1, label: "Top Font Size"})
viewof player_font_size= Inputs.range([0, 30], {value: 23, step: 1, label: "Player Font Size"})
viewof box_opacity= Inputs.range([0, 1], {value: 0.85, step: 0.01, label: "Box Opacity"})
viewof logo_opacity= Inputs.range([0, 1], {value: .35, step: 0.01, label: "Logo Opacity"})
viewof circle_opacity= Inputs.range([0, 1], {value: .9, step: 0.01, label: "Circle Opacity"})
{
const data = top_5;
const svg = d3.create("svg")
.attr("id", "leaderboard")
.attr("viewBox", [-5, -85, 620, 650])
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
.style("background-color", background);
// Define the rectangles
const rectWidth = 140;
const rectHeight = 160;
const rectHeight2 = 350;
const rectPadding = 10;
const rectPaddingY = 20;
const imgWidth = 300;
const imgHeight = 300;
const circleR = 45
const circleCx = rectWidth/2
const rankX= rectWidth/2
const rectData = [
{ x: 10, y: 10, width: rectWidth*4+rectPadding*3, height: rectHeight, img_x: (rectWidth*4+rectPadding*3)/2 , img_height: imgHeight *2,circle_r: circleR+15, circle_cx: 80},
{ x: 10, y: rectHeight + rectPaddingY, width: rectWidth, height: rectHeight2, img_height: imgHeight, circle_r: circleR, circle_cx: circleCx},
{ x: rectWidth + rectPadding * 2, y: rectHeight + rectPaddingY, width: rectWidth, height: rectHeight2, img_height: imgHeight,circle_r: circleR, circle_cx: circleCx},
{ x: rectWidth * 2 + rectPadding * 3, y: rectHeight + rectPaddingY, width: rectWidth, height: rectHeight2, img_height:imgHeight, circle_r: circleR, circle_cx: circleCx},
{ x: rectWidth * 3 + rectPadding * 4, y: rectHeight + rectPaddingY, width: rectWidth, height: rectHeight2, img_height:imgHeight, circle_r: circleR, circle_cx: circleCx},
];
//title
svg.append("text")
.text(pre_title + " LEADERBOARD")
.attr("x", 305)
.attr("y",-35)
.attr("font-size",40)
.attr('fill',title_color)
.attr("font-weight", 'bold')
.attr("font-family", font_family)
.attr("text-anchor","middle")
.attr('alignment-baseling','middle')
function toSentenceCase(str) {
return str.replace(/_/g, ' ').replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
}
//subtitle
svg.append("text")
.text(toSentenceCase(metric))
.attr("x", 305)
.attr("y",-2)
.attr("font-size",30)
.attr('fill',title_color)
.attr("font-family", font_family)
.attr("text-anchor","middle")
.attr('alignment-baseling','middle')
.style("text-transform","uppercase")
const defs = svg.append("defs");
// Define a unique ID for each clipping path
const clipPathId = (d, i) => `clip-${i}`;
const clipPaths = defs.selectAll("clipPath")
.data(rectData)
.enter()
.append("clipPath")
.attr("id", clipPathId); // reuse the same ID as the rectangles
clipPaths.append("rect")
.attr("width", (d) => d.width)
.attr("height", (d) => d.height);
const rectGroup = svg.selectAll("g")
.data(rectData)
.enter()
.append("g")
.attr("transform", (d) => `translate(${d.x},${d.y})`);
rectGroup.append("rect")
.attr("width", (d) => d.width)
.attr("height", (d) => d.height)
.attr("stroke", box_stroke)
.attr("opacity", box_opacity)
.attr("fill", (d, i) => box_color_type==="Mapped" ? data[i].team_color : box_color);
// append team logos
rectGroup.append("image")
.attr("xlink:href", (d,i) => data[i].team_logo)
.attr("x", (d) => (d.width - d.img_height) / 2)
.attr("y", (d) => (d.height - d.img_height) / 2)
.attr("height", (d) => d.img_height)
.attr("width", (d) => d.img_height)
.attr("opacity", logo_opacity)
.attr("clip-path", (d, i) => `url(#${clipPathId(d, i)})`);
rectGroup.append("circle")
.attr("cx", (d) => d.circle_cx)
.attr("cy", (d) => d.height / 2)
.attr("r", d => d.circle_r)
.attr("fill", circle_color)
.attr("id", "clipCircle")
.attr("opacity", circle_opacity);
// Add the image with the clip path applied
rectGroup.append("image")
.attr("xlink:href", (d,i) => data[i].headshot_url)
.attr("x", (d, i) => i === 0 ? 70- d.imgWidth: (d.width - d.circle_r*2.7) / 2)
.attr("y", (d) => (d.height - d.circle_r*2.6)/2)
.attr("height",(d) => d.circle_r*2.7)
.attr("width", (d) => d.circle_r*2.7)
.attr("clip-path", 'circle(36% at 50% 50%)');
//rank
rectGroup.append("text")
.text(function(d, i) { return i+1; })
.attr("x", (d,i) => i === 0 ? 160 : rectWidth/2)
.attr("y", (d,i) => i === 0 ? rectHeight/2 + 10 : 32)
.attr("font-weight", 'bold')
.attr("font-size", (d,i) => i === 0 ? 50 : player_font_size)
.attr("font-family", font_family)
.attr("text-anchor", (d,i) => i === 0 ? "start" : 'middle')
.attr('alignment-baseline', 'middle')
.attr('fill', font_color)
.style("text-shadow", "2px 2px 4px rgba(0, 0, 0, 0.5)");
// player first name
rectGroup.append("text")
.text(function(d, i) { return i === 0 ? data[i].player : data[i].player.split(" ")[0]; })
.attr("x", (d,i) => i === 0 ? 200 : rectWidth/2)
.attr("y", (d,i) => i === 0 ? rectHeight/2 + 10 -14: 65)
.attr("font-weight", 'bold')
.attr("font-size", (d,i) => i === 0 ? top_player_font_size : player_font_size)
.attr("font-family", font_family)
.attr("text-anchor", (d,i) => i === 0 ? "start" : 'middle')
.style("text-transform", (d,i) => i===0 ? "uppercase": 'none')
.attr('alignment-baseline', 'middle')
.attr('fill', font_color)
.style("text-shadow", "2px 2px 4px rgba(0, 0, 0, 0.5)");
// player last name
rectGroup.append("text")
.text(function(d, i) { return i === 0 ? '': data[i].player.split(" ")[1]; })
.attr("x", (d,i) => i === 0 ? 300 : rectWidth/2)
.attr("y", (d,i) => i === 0 ? 10 : 65 + player_font_size*1.4)
.attr("font-weight", 'bold')
.attr('font-size', player_font_size)
.attr("font-family", font_family)
.attr("text-anchor", (d,i) => i === 0 ? "start" : 'middle')
.attr('fill', font_color)
.style("text-transform","uppercase")
.style("text-shadow", "2px 2px 4px rgba(0, 0, 0, 0.5)");
// player team
rectGroup.append("text")
.text(function(d, i) { return i === 0 ? data[i].team_name : data[i].team_abbr})
.attr("x", (d,i) => i === 0 ? 200 : rectWidth/2)
.attr("y", (d,i) => i === 0 ? rectHeight/2 + 10 +14 : 255)
.attr("font-color","white")
.attr("font-weight", 'bold')
.attr("font-size", (d,i) => i === 0 ? 24 : 20)
.attr("font-family", font_family)
.attr("text-anchor", (d,i) => i === 0 ? "start" : 'middle')
.attr('alignment-baseline', 'middle')
.attr('fill', font_color)
.style("text-shadow", "2px 2px 4px rgba(0, 0, 0, 0.5)");
// metric
rectGroup.append("text")
.text(function(d, i) { return data[i][metric]; })
.attr("x", (d,i) => i === 0 ? 560 : rectWidth/2)
.attr("y", (d,i) => i === 0 ? rectHeight/2+10 : 315)
.attr("font-weight", 'bold')
.attr("font-size", (d,i) => i === 0 ? 30 : player_font_size)
.attr("font-family", font_family)
.attr("text-anchor", (d,i) => i === 0 ? "end" : 'middle')
.attr('alignment-baseline', 'middle')
.attr('fill', font_color)
.style("text-shadow", "2px 2px 4px rgba(0, 0, 0, 0.5)");
svg.append("text")
.text("")
.attr("x", 300)
.attr("y", 300)
.attr("font-size", 140)
.attr("fill", "white")
.attr("text-anchor", "middle")
.attr("opacity", 0.22)
.attr("transform", "rotate(45, " + 300 + ", " + 300 + ")");
svg.append("text")
.text("Graphic: Tanya Shapiro")
.attr("x", 10)
.attr("y", 552)
.attr("font-size", 16)
.attr("fill","white")
.attr("font-family", font_family)
svg.append("text")
.text("Data: ESPN")
.attr("x", 600)
.attr("y", 552)
.attr("text-anchor", "end")
.attr("font-size", 16)
.attr("fill","white")
.attr("font-family", font_family)
return svg.node();
}