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'
})
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)
filteredAggStats = calculateStats(aggButtons, filteredStats)




Reactable.setData('nba', filteredAggStats)
Reactable.setHiddenColumns('nba', inverseValues)
defaultKeys = ['name', 'pos', 'team_abbr'];

keysToKeep = defaultKeys.concat(selectedValues);

csvData = filteredAggStats.map(obj => {
  return Object.fromEntries(
    Object.entries(obj).filter(([key]) => keysToKeep.includes(key))
  );
});
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);
}
viewof selectedSeason = Inputs.select([2023, 2024], {label: "Season", value:2024,  format: x => x.toString()})
dropdownButton("positions", "Positions", "Select Positions...", viewof selectedPositions)
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"})
viewof downloadButton = Inputs.button("Download CSV" , {reduce: downloadCSV})
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};
{{< fa envelope title="An envelope" >}}
 
    Created with Quarto
    Copyright © 2023 Tanya Shapiro. All rights reserved.
Cookie Preferences