We use cookies

We use cookies and other tracking technologies to improve your browsing experience on our website, to show you personalized content and targeted ads, to analyze our website traffic, and to understand where our visitors are coming from.

Tanya Shapiro
  • Home
  • About
  • Talks
  • Blog
  • Projects

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.
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"
mlb_stats = Array(95) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
nba_stats = Array(107) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
nhl_stats = Array(99) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
wnba_stats = Array(65) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
f1_stats = Array(20) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
results = Array(107) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
metrics = Array(18) ["3-Point Field Goal Percentage", "Assists Per Game", "Average 3-Point Field Goals Attempted", "Average 3-Point Field Goals Made", "Average Field Goals Attempted", "Average Field Goals Made", "Average Free Throws Attempted", "Average Free Throws Made", "Blocks Per Game", "Double Double", "Field Goal Percentage", "Free Throw Percentage", "Minutes Per Game", "Points Per Game", "Rebounds Per Game", "Steals Per Game", "Triple Double", "Turnovers Per Game"]
top_5 = Array(5) [Object, Object, Object, Object, Object]
pre_title = "NBA"
default_stat = "Points Per Game"

Stats

viewof league =  Inputs.select(new Map([["NBA", "nba_stats"], ["MLB", "mlb_stats"],["NHL","nhl_stats"],["WNBA","wnba_stats"],["F1","f1_stats"]]), {label: "League", value:'nba_stats'})
viewof metric = Inputs.select(metrics, {label: "Metric", value:default_stat})
league = "nba_stats"
metric = "Points Per Game"

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"})
box_color_type = "Mapped"
box_color = "black"
box_stroke = "none"
background = "#1E252C"
font_family = "Oswald"
circle_color = "#EEF0F2"
font_color = "white"
title_color = "white"
top_player_font_size = 32
player_font_size = 23
box_opacity = 0.85
logo_opacity = 0.35
circle_opacity = 0.9
{
  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();

}
NBA LEADERBOARDPoints Per Game1Joel EmbiidPhiladelphia 76ers33.12LukaDoncicDAL32.43DamianLillardPOR32.24ShaiGilgeous-A.OKC31.45GiannisAntetokoun.MIL31.1Graphic: Tanya ShapiroData: ESPN
 
    Created with Quarto
    Copyright © 2023 Tanya Shapiro. All rights reserved.
Cookie Preferences