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

NBA Player Stats

app
sports
reactable
Observable
Explore NBA player stats with this easy to query app. Data from ESPN and basketball-reference.com.
base_url = "https://a.espncdn.com/combiner/i?img=/i/teamlogos/nba/500/"

teams = [{value: 'ATL', label:'ATL Hawks'},
 {value: 'BKN', label: 'BKN Nets'},
 {value: 'BOS', label: 'BOS Celtics'},
 {value: 'CHA', label: 'CHA Hornettes'},
 {value: 'CHI', label: 'CHI Bulls'},
 {value: 'CLE', label: 'CLE Cavaliers'},
  {value:'DAL', label: 'DAL Mavericks'},
   {value: 'DEN', label:'DEN Nuggets'},
  {value: 'DET', label: 'DET Pistons'},
   {value: 'GSW', label: 'GS Warriors'},
 {value: 'HOU', label: 'HOU Rockets'},
  {value: 'IND', label: 'IND Pacers'},
 {value: 'LAC', label: 'LA Clippers'},
  {value: 'LAL', label: 'LA Lakers'},
  {value: 'MEM', label: 'MEM Grizzlies'},
 {value:'MIA', label:'MIA Heat'},
  {value:'MIL', label:'MIL Bucks'},
  {value:'MIN', label:'MIN Timberwolves'},
 {value: 'NO', label: 'NO Pelicans'},
  {value:'NYK', label:'NYK Knicks'},
   {value: 'OKC', label: 'OKC Thunder'},
 {value: 'ORL', label: 'ORL Magic'},
  {value: 'PHI', label: 'PHI 76ers'},
 {value: 'PHO', label: 'PHO Suns'},
 {value:'POR', label: 'POR Trail Blazers'},
   {value: 'SAC', label: 'SAC Kings'},
 {value: 'SAS', label: 'SAS Spurs'},
  {value: 'TOR', label: 'TOR Raptors'},
 {value: 'UTAH', label: 'UTAH Jazz'},
  {value: 'WAS', label: 'WAS Wizards'}]
 
 positions = [
  {value:'C', label: 'C'},
  {value:'PF', label: 'PF'},
 {value:'PG', label: 'PG'},
 {value:'SF', label: 'SF'},
 {value:'SG', label: 'SG'}]
 
 
  values = [
 {value:'pts', label: 'PTS'},
 {value:'fga', label: 'FGA'},
 {value:'fgm', label: 'FGM'},
 {value:'fg_percent', label:'FG %'},
   {value: 'x2pa', label:'2PA'},
 {value: 'x2p', label:'2P'},
  {value: 'x2p_percent', label:'2P %'},
  {value: 'x3pa', label:'3PA'},
 {value: 'x3p', label:'3P'},
  {value: 'x3p_percent', label:'3P %'},
{value:'fta', label: 'FTA'},
 {value:'ft', label: 'FT'},
 {value: 'ft_percent', label:'FT %'}]

teamFormat = teams.map(d => ({value: d.value, label: "<div class='team-option' style='height:100%;'><img style='height:100%;;padding-right:5px;vertical-align:middle;'src="+base_url+d.value+".png><span style='vertical-align:middle;'></div>"+d.value+"</span></div>"}))




viewof selectedTeams = checkbox({
  options: teamFormat,
  value: teams.map(team => team.value),
  orientation: 'vertical'
})

viewof selectedPositions = checkbox({
  options: positions,
  value: ["PG", "PF", "C","SF","SG"],
  orientation: 'vertical'
})

viewof selectedValues = checkbox({
  options: values,
  value: ["fga",'fgm', "fg_percent", "pts"],
  orientation: 'vertical'
})
base_url = "https://a.espncdn.com/combiner/i?img=/i/teamlogos/nba/500/"
teams = Array(30) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
positions = Array(5) [Object, Object, Object, Object, Object]
values = Array(13) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object]
teamFormat = Array(30) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
selectedTeams = Array(30) ["ATL", "BKN", "BOS", "CHA", "CHI", "CLE", "DAL", "DEN", "DET", "GSW", "HOU", "IND", "LAC", "LAL", "MEM", "MIA", "MIL", "MIN", "NO", "NYK", …]
selectedPositions = Array(5) ["C", "PF", "PG", "SF", "SG"]
selectedValues = Array(4) ["pts", "fga", "fgm", "fg_percent"]
data = transpose(sample)

// Get all keys from the data array
allKeys = Object.keys(data[0]); // Assuming data is not empty

// Create a new array of inverse values from the selected values
updatedSelectedValues = [...selectedValues, 'player_html','gp'];inverseValues = allKeys.filter(key => !updatedSelectedValues.includes(key));

filteredStats = data.filter(d => selectedTeams.includes(d.team_abbr) & selectedPositions.includes(d.pos) & d.season === selectedSeason)

function calculateStats(type, statsArray) {
  if (type === 'avg') {
    const avgStats = statsArray.map(stat => ({
      ...stat,
      pts: Math.round((stat.pts / stat.gp) * 10) / 10,
      fga: Math.round((stat.fga / stat.gp) * 10) / 10,
      fgm: Math.round((stat.fgm / stat.gp) * 10) / 10,
      ft: Math.round((stat.ft / stat.gp) * 10) / 10,
      fta: Math.round((stat.fta / stat.gp) * 10) / 10,
      x3p: Math.round((stat.x3p / stat.gp) * 10) / 10,
      x3pa: Math.round((stat.x3pa / stat.gp) * 10) / 10,
      x2p: Math.round((stat.x2p / stat.gp) * 10) / 10,
      x2pa: Math.round((stat.x2pa / stat.gp) * 10) / 10
    }));
    return avgStats;
  } else if (type === 'total') {
    // Calculate total stats (simply return the same array for 'total' type)
    return statsArray;
  } else {
    return "Invalid type. Please provide 'avg' or 'total'.";
  }
}

function updateGroupButton(value) {
  const buttons = document.querySelectorAll('.oi-ec050e button');

  buttons.forEach(button => {
    if (value === "avg" && button.textContent === "Game Avg") {
      button.textContent = "Game Avg ✓";
    } else if (value === "total" && button.textContent === "Total") {
      button.textContent = "Total ✓";
    } else if ((value !== "avg" && button.textContent === "Game Avg ✓") ||
               (value !== "total" && button.textContent === "Total ✓")) {
      button.textContent = button.textContent.slice(0, -2); // Remove 'X' if value changes
    }
  });
}


updateGroupButton(aggButtons)
data = Array(1045) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
allKeys = Array(21) ["player_html", "name", "team_abbr", "pos", "team_logo", "player_image", "gp", "fga", "fgm", "fg_percent", "x2pa", "x2p", "x2p_percent", "x3pa", "x3p", "x3p_percent", "fta", "ft", "ft_percent", "pts", …]
updatedSelectedValues = Array(6) ["pts", "fga", "fgm", "fg_percent", "player_html", "gp"]
inverseValues = Array(15) ["name", "team_abbr", "pos", "team_logo", "player_image", "x2pa", "x2p", "x2p_percent", "x3pa", "x3p", "x3p_percent", "fta", "ft", "ft_percent", "season"]
filteredStats = Array(505) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
calculateStats = ƒ(type, statsArray)
updateGroupButton = ƒ(value)
undefined
filteredAggStats = calculateStats(aggButtons, filteredStats)




Reactable.setData('nba', filteredAggStats)
filteredAggStats = Array(505) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
Error: reactable instance 'nba' not found
OJS Error

Error: reactable instance 'nba' not found

Reactable.setHiddenColumns('nba', inverseValues)
Error: reactable instance 'nba' not found
OJS Error

Error: reactable instance 'nba' not found

defaultKeys = ['name', 'pos', 'team_abbr'];

keysToKeep = defaultKeys.concat(selectedValues);

csvData = filteredAggStats.map(obj => {
  return Object.fromEntries(
    Object.entries(obj).filter(([key]) => keysToKeep.includes(key))
  );
});
defaultKeys = Array(3) ["name", "pos", "team_abbr"]
keysToKeep = Array(7) ["name", "pos", "team_abbr", "pts", "fga", "fgm", "fg_percent"]
csvData = Array(505) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
function convertToCSV(array) {
  const header = Object.keys(array[0]).join(',') + '\n';
  const rows = array.map(obj => Object.values(obj).join(',')).join('\n');
  return header + rows;
}

// Function to trigger the download
function downloadCSV() {
  const csvContent = convertToCSV(csvData);
  const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });

  const link = document.createElement('a');
  const url = URL.createObjectURL(blob);
  link.setAttribute('href', url);
  link.setAttribute('download', 'nba-stats.csv');
  link.style.display = 'none';
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}
convertToCSV = ƒ(array)
downloadCSV = ƒ()
viewof selectedSeason = Inputs.select([2023, 2024], {label: "Season", value:2024,  format: x => x.toString()})
dropdownButton("positions", "Positions", "Select Positions...", viewof selectedPositions)
selectedSeason = 2024
dropdownButton("teams", "Teams", "Select Teams...", viewof selectedTeams)
dropdownButton("values", "Stats", "Select Stats..", viewof selectedValues)
viewof aggButtons = Inputs.button([
["Total", value => "total"],
["Game Avg", value=>"avg"]],
{value: "total"})
aggButtons = "total"
viewof downloadButton = Inputs.button("Download CSV" , {reduce: downloadCSV})
downloadButton = 0
d3format = require("d3-format@1")

function checkbox(config = {}) {
  let {
    value: formValue,
    title,
    description,
    submit,
    orientation = "horizontal",
    disabled,
    options
  } = Array.isArray(config) ? { options: config } : config;
  options = options.map(o =>
    typeof o === "string" ? { value: o, label: o } : o
  );

  const buttons = html`<div class='action-buttons' style='padding:0px 2px;margin-bottom:12px;display:flex;justify-content:space-between;'>
    <button id="selectAllBtn" style='width:50%;border-radius:4px 0px 0px 4px;border:1px #ccc solid;border-right:none!important;height:25px;background-color:whitesmoke;color:black;font-size:12px;height:30px;box-shadow:none;'>Select All</button>
    <button id="clearAllBtn" style='width:50%;border-radius:0px 4px 4px 0px;border:1px #ccc solid;box-shadow:none;height:25px;background-color:whitesmoke;color:black;font-size:12px;height:30px;'>Clear All</button>
  </div>`;

 const selectAllBtn = buttons.querySelector('#selectAllBtn');
selectAllBtn.addEventListener('click', () => {
  const checkboxes = form.querySelectorAll('input[type="checkbox"]');
  checkboxes.forEach(checkbox => {
    checkbox.checked = true;
  });
  updateFormValue(); 
});

const clearAllBtn = buttons.querySelector('#clearAllBtn');
clearAllBtn.addEventListener('click', () => {
  const checkboxes = form.querySelectorAll('input[type="checkbox"]');
  checkboxes.forEach(checkbox => {
    checkbox.checked = false;
  });
  updateFormValue(); 
});

const updateFormValue = () => {
  const checkboxes = form.querySelectorAll('input[type="checkbox"]');
  const selectedValues = Array.from(checkboxes)
    .filter(checkbox => checkbox.checked)
    .map(checkbox => checkbox.value);
  form.value = selectedValues;

  // Trigger the input event to signal the value change
  form.dispatchEvent(new Event('input', { bubbles: true }));
};


  
  const form = input({
    type: "checkbox",
    title,
    description,
    submit,
    getValue: input => {
      if (input.length)
        return Array.prototype.filter
          .call(input, i => i.checked)
          .map(i => i.value);
      return input.checked ? input.value : false;
    },
    form: html`
      <form>
        ${buttons}
        <div class='option-labels' style='padding:2px 8px;'>
        ${options.map(({ value, label }, i) => {
          const input = html`<input type=checkbox name=input ${
            (formValue || []).indexOf(value) > -1 ? "checked" : ""
          } style="vertical-align: top;margin-right:6px;" />`;
          input.setAttribute("value", value);
          if (disabled) input.setAttribute("disabled", disabled);
          const tag = html`<label style="display:${orientation === 'horizontal' ? `inline-block` : `block`};margin: 5px 10px 3px 0; font-size: 0.85em;display:flex;align-items:center;height:30px;">
           ${input}
           ${label}
          </label>`;
          return tag;
        })}
        </div>
      </form>
    `
  });

  
  form.output.remove();
  
  return form;
};


function input(config) {
  let {
    form,
    type = "text",
    attributes = {},
    action,
    getValue,
    title,
    description,
    format,
    display,
    submit,
    options
  } = config;
  const wrapper = html`<div></div>`;
  if (!form)
    form = html`<form>
    <input name=input type=${type} />
  </form>`;
  Object.keys(attributes).forEach(key => {
    const val = attributes[key];
    if (val != null) form.input.setAttribute(key, val);
  });
  if (submit)
    form.append(
      html`<input name=submit type=submit style="margin: 0 0.75em" value="${
        typeof submit == "string" ? submit : "Submit"
      }" />`
    );
  form.append(
    html`<output name=output style="font: 14px Menlo, Consolas, monospace; margin-left: 0.5em;"></output>`
  );
  if (title)
    form.prepend(
      html`<div style="font: 700 0.9rem sans-serif; margin-bottom: 3px;">${title}</div>`
    );
  if (description)
    form.append(
      html`<div style="font-size: 0.85rem; font-style: italic; margin-top: 3px;">${description}</div>`
    );
  if (format)
    format = typeof format === "function" ? format : d3format.format(format);
  if (action) {
    action(form);
  } else {
    const verb = submit
      ? "onsubmit"
      : type == "button"
      ? "onclick"
      : type == "checkbox" || type == "radio"
      ? "onchange"
      : "oninput";
    form[verb] = e => {
      e && e.preventDefault();
      const value = getValue ? getValue(form.input) : form.input.value;
      if (form.output) {
        const out = display ? display(value) : format ? format(value) : value;
        if (out instanceof window.Element) {
          while (form.output.hasChildNodes()) {
            form.output.removeChild(form.output.lastChild);
          }
          form.output.append(out);
        } else {
          form.output.value = out;
        }
      }
      form.value = value;
      if (verb !== "oninput")
        form.dispatchEvent(new CustomEvent("input", { bubbles: true }));
    };
    if (verb !== "oninput")
      wrapper.oninput = e => e && e.stopPropagation() && e.preventDefault();
    if (verb !== "onsubmit") form.onsubmit = e => e && e.preventDefault();
    form[verb]();
  }
  while (form.childNodes.length) {
    wrapper.appendChild(form.childNodes[0]);
  }
  form.append(wrapper);
  return form;
};




function dropdownButton(id, labelText, buttonName, content) {

function toggleDropdown() {
    const dropdown = document.getElementById(id+'-content');

    // Check the current display property
    if (dropdown.style.display === "none" || dropdown.style.display === "") {
        dropdown.style.display = "block"; // Display the dropdown if it's hidden
        // Add an event listener to the document body to detect clicks
        document.body.addEventListener('click', closeDropdownOnClickOutside);
    } else {
        dropdown.style.display = "none"; // Hide the dropdown if it's displayed
        // Remove the event listener when the dropdown is hidden
        document.body.removeEventListener('click', closeDropdownOnClickOutside);
    }
}


  function closeDropdownOnClickOutside(event) {
        const thisButton = document.getElementById(id);
        const thisDropDown = document.getElementById(id + '-content');

        // Check if the click is within the dropdown content
        const dropdownContent = document.querySelector(`#${id}-content .inner-content`);
        if (!thisButton.contains(event.target) && event.target !== thisButton && !dropdownContent.contains(event.target)) {
            thisDropDown.style.display = 'none';
            document.body.removeEventListener('click', closeDropdownOnClickOutside);
        }
    }


 // const initialButtonName = findSelectedLabels();
  
  const initialButtonName = findSelectedValues() === buttonName ? buttonName : html`${findSelectedLabels()}`;
  
  const initialButtonColor = findSelectedValues() === buttonName ? 'grey' : 'black';
  
  const dropdownButton = htl.html`
  <label style='font-size:14px;font-weight:bold;'>${labelText}</label>
  <div class='dropdown' id=${id}>
      <button onclick=${toggleDropdown} class="dropbtn" style='height:35px;width:100%;padding:4px 2px;background-color:white;border:1px #ccc solid!important;border-radius:4px;color:black;font-size:14px;box-shadow:none;'>
        <span class='dropdown-inner' style='display:flex;justify-content:space-between;height:100%;'>
          <div class='button-name' style='height:100%;display:flex;align-items:center;padding-left:5px;text-wrap:nowrap!important;overflow:hidden;text-overflow:ellipsis;color:${initialButtonColor};'>${initialButtonName}</div>
          <div class='chevron' style='padding-right:5px;padding-left:5px;'><i class="fa-solid fa-chevron-down"></i></div>
        </div>
      </button>
    <div class='dropdown-content' id=${id+'-content'} style='position:absolute;z-index:2000;background-color:white;padding:5px 0px;display:none;width:100%;border:1px #ccc solid;border-radius:2px;box-shadow:0 6px 12px rgba(0,0,0,.175);margin-top:5px;max-height:350px;overflow-y:scroll;'>
        <div style='margin:10px 5px 5px 5px;' class='inner-content'>${content}</div>
    </div>
  </div>`
  

function findSelectedValues() {
      const checkboxes = content.querySelectorAll(`input[type='checkbox']:checked`);
        const selectedValues = Array.from(checkboxes).map(checkbox => checkbox.value);
    
    selectedValues.sort();
    
    if (selectedValues.length === 0) {return buttonName} 
    else {return selectedValues.join(', ')}
  
}

function extractLabelText(labelElement) {
  const imgElement = labelElement.querySelector('img');
  if (imgElement) {
    // Complex label structure with image and text
    const clonedLabel = labelElement.cloneNode(true);
    const checkbox = clonedLabel.querySelector('input[type="checkbox"]');
    if (checkbox) {
      checkbox.remove(); // Remove the checkbox from cloned label
    }
    return clonedLabel.innerHTML.trim();
  } else {
    // Simple label structure with only text
    return labelElement.textContent.trim();
  }
}

function findSelectedLabels() {
  const checkboxes = content.querySelectorAll(`input[type='checkbox']:checked`);
  const selectedLabels = Array.from(checkboxes).map(checkbox => {
    const labelElement = checkbox.closest('label');
    return extractLabelText(labelElement);
  });

  selectedLabels.sort();

  if (selectedLabels.length === 0) {
    return buttonName; // Assuming buttonName is defined somewhere
  } else {
    return selectedLabels.join(', ');
  }
}


  
function updateButtonName() {
  const selectedLabels = findSelectedLabels();
  const button = document.querySelector(`#${id} .button-name`);

  if (selectedLabels === buttonName) {
    button.innerHTML = buttonName; // Preserve any HTML content
    button.style.color = 'grey';
  } else {
    button.innerHTML = selectedLabels; // Set HTML content
    button.style.color = 'black';
  }
}

    // Event listener for checkboxes change
    document.addEventListener('change', function(event) {
        if (event.target.matches(`#${id}-content input[type='checkbox']`)) {
            updateButtonName();
        }
    });
    
        // Event listener for checkboxes change
    document.addEventListener('click', function(event) {
        if (event.target.matches(`#${id}-content .action-buttons button`)) {
            updateButtonName();
        }
    });
    
    
    //updateButtonName();

return dropdownButton};
d3format = Object {format: ƒ(t), formatPrefix: ƒ(t, n), FormatSpecifier: ƒ(t), formatDefaultLocale: ƒ(n), formatLocale: ƒ(t), formatSpecifier: ƒ(t), precisionFixed: ƒ(t), precisionPrefix: ƒ(t, n), precisionRound: ƒ(t, n)}
checkbox = ƒ(…)
input = ƒ(config)
dropdownButton = ƒ(id, labelText, buttonName, content)
{{< fa envelope title="An envelope" >}}
 
    Created with Quarto
    Copyright © 2023 Tanya Shapiro. All rights reserved.
Cookie Preferences