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"
Error: Unable to load file: data/mlb_stats.csv
OJS Error

Error: Unable to load file: data/mlb_stats.csv

Error: Unable to load file: data/nba_stats.csv
OJS Error

Error: Unable to load file: data/nba_stats.csv

Error: Unable to load file: data/nhl_stats.csv
OJS Error

Error: Unable to load file: data/nhl_stats.csv

Error: Unable to load file: data/wnba_stats.csv
OJS Error

Error: Unable to load file: data/wnba_stats.csv

Error: Unable to load file: data/f1_stats.csv
OJS Error

Error: Unable to load file: data/f1_stats.csv

RuntimeError: Unable to load file: data/mlb_stats.csv
OJS Runtime Error

Unable to load file

RuntimeError: Unable to load file: data/mlb_stats.csv
OJS Runtime Error

Unable to load file

RuntimeError: Unable to load file: data/mlb_stats.csv
OJS Runtime Error

Unable to load file

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"
RuntimeError: Unable to load file: data/mlb_stats.csv
OJS Runtime Error

Unable to load file

RuntimeError: Unable to load file: data/mlb_stats.csv
OJS Runtime Error

Unable to load file

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();

}
RuntimeError: Unable to load file: data/mlb_stats.csv
OJS Runtime Error

Unable to load file

 
    Created with Quarto
    Copyright © 2023 Tanya Shapiro. All rights reserved.
Cookie Preferences