From a6c962c35b12b0ae7895ebb9a2f150fb011f522c Mon Sep 17 00:00:00 2001 From: Anna Duan Date: Wed, 23 Oct 2024 22:20:28 -0400 Subject: [PATCH 1/9] Dashboard project --- .DS_Store | Bin 0 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..da952147d170f6ac70add92a3be30a5e77cc1ebc GIT binary patch literal 6148 zcmeHK&u`N(6n^f?mQaw21kx^`NL)uL0}UoFp>zihTowceKqXnKEo*95O-qrgN;$(H z!wuB5qU_A^{dFv5pL&nMryXk z2MQgdPZR3XJxY)}aSS*H{&fa;@7|#p`}T-(?Aky3SJDCMhG-kpfHIPFh@G9HT%bN7 zrCZV$xjIO4l^_luBF~UozqCNRWb9;CjBM<0$tcf?NxS_+)Yj@();GKjui?Fqj?^rg z#FJ^!iN|lb^h&8bInm?fb=IHtnm3;-If*mbA8Us!?Zf2Vn@px^)=^WL7TPy54W94$ zz2@e8zWsQ2D|qx|XR#H`cemTnAMPv`zIW^H{iplAK|Yk~vmPZ0oPS!bTRed;P%g#q z$7n2bB@YpY`|vPFpTPW(MtZ)A3jbeWv&{cGEEQ&QKvNp6+KSs1+Z%`(Yugxe&F6S- zeOq*lSyv=05qNaVyV1W1gb#nPZYI*{on0I-f~EztQlfPDgsp~cc5 zS|Gx>0*$M%PYhw)Q6E^l&|+!OxRbDt4`F*2_JtyJ?}#60a}uFJmpcX=1FH Date: Wed, 23 Oct 2024 22:23:20 -0400 Subject: [PATCH 2/9] dashboard files --- .DS_Store | Bin 6148 -> 8196 bytes css/.DS_Store | Bin 0 -> 6148 bytes css/styles.css | 122 +++++++++++++++++++++++ data/.DS_Store | Bin 0 -> 6148 bytes data/group_means_2024.json | 181 ++++++++++++++++++++++++++++++++++ data/stats_2024.json | 192 +++++++++++++++++++++++++++++++++++++ index.html | 58 +++++++++++ js/barchart.js | 122 +++++++++++++++++++++++ js/chart_data.js | 111 +++++++++++++++++++++ js/main.js | 37 +++++++ js/stat_entry.js | 145 ++++++++++++++++++++++++++++ www/.DS_Store | Bin 0 -> 6148 bytes www/penn_shield.svg | 1 + 13 files changed, 969 insertions(+) create mode 100644 css/.DS_Store create mode 100644 css/styles.css create mode 100644 data/.DS_Store create mode 100644 data/group_means_2024.json create mode 100644 data/stats_2024.json create mode 100644 index.html create mode 100644 js/barchart.js create mode 100644 js/chart_data.js create mode 100644 js/main.js create mode 100644 js/stat_entry.js create mode 100644 www/.DS_Store create mode 100644 www/penn_shield.svg diff --git a/.DS_Store b/.DS_Store index da952147d170f6ac70add92a3be30a5e77cc1ebc..8e09716f56a4927b90859f1e5bb311609c8d2b2a 100644 GIT binary patch delta 690 zcmZoMXmOBWU|?W$DortDU;r^WfEYvza8E20o2aKKEDn+f@)`1zlXCKtKvJ6pImB2t zD{%BPFJ|Z9nApH#2vWq%kjzlbQ0$q5PzV%V55$lELjgk7}IAWItr!+ll560 z+@ZS5f{XHU^7GPxDuI?VurQ=BBr=pRBx0J%n22O5TpiHd~xIjI`@lQHf@?qRDN1!4k`4qhJ1sS-^W@1R3yoS|2mWv^iArI(*RE7!$J%$XR zH*y(rP(8xv3G`rve1K&Q5NVdBGAW)p`N>H+ z`AJYK9f7poe=q>D2%0U2-|)$L!pChXa^B=gLiU?EMfeyumhmwzX6N7#WCpv7L4X@b gyMn@KW8rt^$^0^&pb%qVf`lF@Kp8g2^UPre04g{_{{R30 diff --git a/css/.DS_Store b/css/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0dbe384bcb6eb37d20cbf28fc42cf99bbab04853 GIT binary patch literal 6148 zcmeHKF-`+P474Fd1WiiHeL*CCu!@p`nh&4^x|31_3c9QE9lpTN!i;SpqJsvB28|_q zcDr&c%|yClb2(!ZSV&;Yu@2B ptb>9O?HCyC7#p_Zn<&b<#x>4+;g}e7p8@J3lLCLOzz6pQ91j2h literal 0 HcmV?d00001 diff --git a/css/styles.css b/css/styles.css new file mode 100644 index 0000000..3d51276 --- /dev/null +++ b/css/styles.css @@ -0,0 +1,122 @@ +body { + font-family: 'Poppins', sans-serif; + color: #011f5b; +} + +#page-name { + font-size: 3rem; + font-weight: 100; + color: #011f5b; + margin-left: 1rem; +} + +.header { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + color: #011f5b; + font-size: 0.6rem; +} + +.header img { + margin-left: 1rem; + width: 4rem; + align-self: center; +} + +.benchmark-page { + display: grid; + grid-template-columns: repeat(6, 1fr); + grid-template-rows: repeat(4, min-content); + gap: 40px; + align-content: flex-start; +} + +.benchmark-menu { + grid-column: 1 / 2; +} + +fieldset { + width: 100%; + height: auto; + text-align: left; + text-justify: auto; + display: flex; + flex-direction: column; + background-color: rgba(0, 0, 0, 0.05); + border-color: transparent; + border-radius: 0.5em; +} + +.benchmark-menu input { + border-color: transparent; + font-size: 1rem; + color: gray; + opacity: 0.5; + font-weight:lighter; +} + +select { + width: 100%; + padding: 0.5rem; + font-size: 1rem; + font-weight: 100; + color: #011f5b; + border: transparent; + border-radius: 0.5em; +} + +ul { + list-style-type: none; + font-weight: 100; + font-size: 1rem; + text-align: left; +} + +.input-wrapper { + position: relative; + width: 60%; + display: flex; + flex-direction: row; + align-items: center; + background-color: white; + opacity: 0.75; + border-radius: 0.5rem; +} + +input[type="number"] { + width: 100%; + padding-right: 4rem; + box-sizing: border-box; + appearance: textfield; +} + +.unit { + position: absolute; + right: 0; + width: 4rem; + color: #cdcbcb; + pointer-events: none; + font-size: 0.9em; + z-index: 1; +} + +.benchmark-chart-area { + display: flex; + flex-direction: column; + grid-column: 2 / 7; + grid-row: 1 / 4; + justify-content: flex-start; +} + +#position-title { + margin-left: 5rem; + font-size: 2rem; + font-weight: 100; + color: #011f5b; +} + +input[type="number"]:invalid { + color: red; +} \ No newline at end of file diff --git a/data/.DS_Store b/data/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..91a1dd296f7b0cf3c7ddc494c285314e8eb7b696 GIT binary patch literal 6148 zcmeHK%Sr=55UkN00v?i!;BmensN@625D%XH05vLt#H^U)zJKH=YE}0rt}b{|M7p87 zdV0ENb_#YL0McxGa|Db545`8-r$-d-?b@ly!enDS;sOu2#T8aP1O3I2zI`8ectV3M zK7Rk2YdOXtb&3h@v1Z2VY}s5dmd%1yqU?k^c`=~}jF`3I>NmvitDiqr+Yn)bfnXpQ z2nK?IA7+4Owq&^H7&aIP27-Zi2DCq9s<2dS9c}BN(e+vV1EGYzF2=%Ku~cjw*+WTQ zO7zlHpBTx@8P7GZRBRo+9LZ+pICbW?$4j!=8PArER2;(w1HnL-fn9Hob^f37m+5`v zcT1ub3 + + + + + Penn Football Strength Benchmarking + + + + +
+ + +
+
+
+

Select athlete position

+
+
+

Enter athlete stats to benchmark against position group

+
    +
  • + +
  • +
  • + +
  • +
+
+
+ +
+

Select a position group to start

+
+ +
+
+
+
+ + + + diff --git a/js/barchart.js b/js/barchart.js new file mode 100644 index 0000000..8ded83d --- /dev/null +++ b/js/barchart.js @@ -0,0 +1,122 @@ +import { Chart } from 'https://cdn.jsdelivr.net/npm/chart.js@4.4.4/auto/+esm'; + +const chartInstances = {}; + +function initChart(chartEl, positionMedians, statNames, playerStats, playerPercentiles) { + + if (chartInstances[chartEl.id]) { + chartInstances[chartEl.id].destroy(); + } + + const data = { + labels: statNames, + datasets: [ + { + label: 'Player Percentiles', + data: playerPercentiles, + backgroundColor: getColor(), + borderColor: getColor(), + borderWidth: 1, + barThickness: 'flex', + maxBarThickness: 70, + borderRadius: 10, + borderSkipped: false, + minBarLength: 10 + } + ] + }; + + const options = { + plugins: { + title: { + display: true, + text: "Athlete Percentiles", + }, + legend: { + display: true, + labels: { + generateLabels: function (chart) { + return [ + { + text: '75-100th Percentile', + fillStyle: 'rgba(0, 139, 139, 0.5)', + strokeStyle: 'rgba(0, 139, 139, 1)', + lineWidth: 1, + hidden: false + }, + { + text: '50-75th Percentile', + fillStyle: 'rgba(255, 215, 0, 0.5)', + strokeStyle: 'rgba(255, 215, 0, 1)', + lineWidth: 1, + hidden: false + }, + { + text: '0-50th Percentile', + fillStyle: 'rgba(250, 128, 114, 0.5)', + strokeStyle: 'rgba(250, 128, 114, 1)', + lineWidth: 1, + hidden: false + } + ]; + } + } + }, + tooltip: { + usePointStyle: true, + callbacks: { + label: function (context) { + const index = context.dataIndex; + const statName = context.label; + const playerStat = playerStats[index]; + const positionMedian = positionMedians[index]; + const percentile = playerPercentiles[index]; + + return [ + `${statName}`, + `Player Stat: ${playerStat}`, + `Position Median: ${positionMedian}`, + `Percentile: ${percentile}%` + ]; + }, + labelPointStyle: function (context) { + return { + pointStyle: 'star' + }; + } + } + } + }, + indexAxis: 'y', + aspectRatio: 2, + scales: { + x: { + beginAtZero: true, + min: 0, + max: 100 + } + }, + elements: { + bar: { + minBarLength: 40 + } + }, + responsive: true + }; + + chartInstances[chartEl.id] = new Chart(chartEl, { type: 'bar', data, options }); + + function getColor() { + return playerPercentiles.map(percentile => { + if (percentile >= 75) { + return 'rgba(0, 139, 139, 0.5)'; + } else if (percentile >= 50) { + return 'rgba(255, 215, 0, 0.5)'; + } else { + return 'rgba(250, 128, 114, 0.5)'; + } + }); + } +} + +export { initChart }; diff --git a/js/chart_data.js b/js/chart_data.js new file mode 100644 index 0000000..07e2e55 --- /dev/null +++ b/js/chart_data.js @@ -0,0 +1,111 @@ +function calculateChartData(indivStats, events) { + let playerPosition = "Defensive Back"; + let playerStats = []; + let playerStatsValues = []; + let positionStatsValues = []; + let positionMedians = []; + let playerPercentiles = []; + + const inversePercentileStats = [ + 'Flying 10', '10-Yard Dash', '60-Yard Shuttle', 'L Drill', 'Pro Agility' + ]; + + function getPercentiles() { + playerPercentiles = []; + + playerStats.forEach((statName, index) => { + const playerValue = playerStatsValues[index]; + const statValues = positionStatsValues[index]; + const sortedStatValues = statValues.slice().sort((a, b) => a - b); + const countBelow = sortedStatValues.filter(value => value <= playerValue).length; + + let percentile = (countBelow / sortedStatValues.length); + percentile = Math.round(percentile * 100); + + if (inversePercentileStats.includes(statName)) { + percentile = 100 - percentile; + } + + playerPercentiles.push(percentile); + }); + + return playerPercentiles; + } + + function getMedians() { + positionMedians = []; + + positionStatsValues.forEach(statValues => { + const sortedValues = statValues.slice().sort((a, b) => a - b); + const mid = Math.floor(sortedValues.length / 2); + + let median; + if (sortedValues.length % 2 === 0) { + median = (sortedValues[mid - 1] + sortedValues[mid]) / 2; + } else { + median = sortedValues[mid]; + } + positionMedians.push(median); + }); + + return positionMedians; + } + + function updatePositionStatsValues() { + positionStatsValues = playerStats.map(statName => { + return indivStats[playerPosition].map(item => item[statName]); + }); + + positionStatsValues = positionStatsValues.map(statArray => statArray.flat(1)); + getMedians(); + getPercentiles(); + } + + events.addEventListener('positionSelected', (evt) => { + const { position } = evt.detail; + playerPosition = position; + updatePositionStatsValues(); + }); + + events.addEventListener('statFilled', (evt) => { + const { statName, filled, statValue } = evt.detail; + + if (filled) { + const index = playerStats.indexOf(statName); + + if (index !== -1) { + playerStatsValues[index] = statValue; + positionStatsValues[index] = indivStats[playerPosition].map(item => item[statName]); + } else { + playerStats.push(statName); + playerStatsValues.push(statValue); + positionStatsValues.push(indivStats[playerPosition].map(item => item[statName])); + } + } else { + const index = playerStats.indexOf(statName); + if (index !== -1) { + playerStats.splice(index, 1); + playerStatsValues.splice(index, 1); + positionStatsValues.splice(index, 1); + } + } + positionStatsValues = positionStatsValues.map(statArray => statArray.flat(1)); + getMedians(); + getPercentiles(); + }); + + function getCalculatedData() { + return { + positionMedians, + playerPercentiles, + playerStats, + playerStatsValues, + }; + } + + return { + getCalculatedData, + }; +} + +export { calculateChartData }; diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..7b9d9f7 --- /dev/null +++ b/js/main.js @@ -0,0 +1,37 @@ +import { initChart } from './barchart.js'; +import { initStatEntry } from './stat_entry.js'; +import { calculateChartData } from './chart_data.js'; + +// Fetch data +const indivStatsResponse = await fetch('data/stats_2024.json'); +const indivStats = await indivStatsResponse.json(); + +const events = new EventTarget(); + +const statListEl = document.querySelector('#athlete-stats'); +const positionDropdownEl = document.querySelector('#athlete-stats'); + +const positions = Object.keys(indivStats); +const statNames = Object.keys(Object.values(indivStats)[0][0]); + +// Handle stat entry +initStatEntry(statListEl, positionDropdownEl, statNames, positions, events); + +// Calculate chart data +let chartData = calculateChartData(indivStats, events); + +// Get chart elements +const pctChartEl = document.querySelector('#percentile-chart'); + +// Render charts +function updateCharts() { + const { positionMedians, playerPercentiles, playerStats, playerStatsValues } = chartData.getCalculatedData(); + + initChart(pctChartEl, positionMedians, playerStats, playerStatsValues, playerPercentiles); +} + +// Listen for changes in stat or position +events.addEventListener('statFilled', updateCharts); +events.addEventListener('positionSelected', updateCharts); + +updateCharts(); \ No newline at end of file diff --git a/js/stat_entry.js b/js/stat_entry.js new file mode 100644 index 0000000..e47bbf5 --- /dev/null +++ b/js/stat_entry.js @@ -0,0 +1,145 @@ +function initStatEntry(statListEl, positionRadioEl, stats, positions, events) { + const listEl = statListEl.querySelector('ul'); + const radioEl = positionRadioEl.querySelector('div'); + + const statListItems = {}; + const positionDropdownItems = {}; + positions = positions.sort((a, b) => { + return a.localeCompare(b); + }); + + const orderedStats = [ + 'Bench', 'Squat', 'Power Clean', '225lb Bench', + '10-Yard Dash', 'Vertical Jump (vertec)', 'Vertical Jump (mat)', 'Broad Jump', '60-Yard Shuttle', 'L Drill', 'Pro Agility', 'Flying 10', + 'Height', 'Weight', 'Wingspan' + ]; + + const unitMapping = { + pounds: ['Bench', 'Squat', 'Power Clean', 'Weight'], + reps: ['225lb Bench'], + inches: ['Vertical Jump (vertec)', 'Vertical Jump (mat)', 'Broad Jump', 'Height', 'Wingspan'], + seconds: ['10-Yard Dash', '60-Yard Shuttle', 'L Drill', 'Pro Agility', 'Flying 10'] + }; + + function getUnit(stat) { + if (unitMapping.pounds.includes(stat)) { + return 'pounds'; + } else if (unitMapping.reps.includes(stat)) { + return 'reps'; + } else if (unitMapping.inches.includes(stat)) { + return 'inches'; + } else if (unitMapping.seconds.includes(stat)) { + return 'seconds'; + } + return ''; + } + + function updatePositionTitle(position) { + const titleEl = document.getElementById('position-title'); + if (titleEl) { + titleEl.textContent = `Position Group: ${position}`; + } + } + + function initListItems() { + for (const stat of orderedStats) { + if (stats.includes(stat)) { + const unit = getUnit(stat); + const item = document.createElement('li'); + item.innerHTML = ` + + `; + statListItems[stat] = item; + } + } + } + + function initDropdownItems() { + const selectEl = document.createElement('select'); + selectEl.name = 'position'; + + for (const position of positions) { + const option = document.createElement('option'); + option.value = position; + option.textContent = position; + + selectEl.appendChild(option); + } + + return selectEl; + } + + initDropdownItems(); + initListItems(); + + function populateList(stats) { + listEl.innerHTML = ''; + + for (const stat of orderedStats) { + if (stats.includes(stat)) { + const item = statListItems[stat]; + listEl.append(item); + } + } + } + + function populateDropdown(positions) { + radioEl.innerHTML = ''; + + const dropdown = initDropdownItems(); + radioEl.appendChild(dropdown); + + dropdown.addEventListener('change', handleDropdownChange); + } + + populateDropdown(positions); + populateList(stats); + + function handleNumEntry(evt) { + const numInput = evt.target; + const statName = numInput.name; + const filled = numInput.value !== '' && numInput.value !== null && numInput.value > 0; + const statValue = filled ? parseFloat(numInput.value) : null; + + numInput.setCustomValidity(''); + + if (!numInput.checkValidity() || numInput.value <= 0) { + numInput.setCustomValidity('Please enter a valid number'); + numInput.reportValidity(); + return; + } + + const event = new CustomEvent('statFilled', { + detail: { statName, filled, statValue } + }); + events.dispatchEvent(event); + } + + function handleDropdownChange(evt) { + const selectedPosition = evt.target.value; + + const event = new CustomEvent('positionSelected', { + detail: { position: selectedPosition } + }); + events.dispatchEvent(event); + updatePositionTitle(selectedPosition); + } + + for (const item of Object.values(statListItems)) { + const numInput = item.querySelector('input'); + numInput.addEventListener('input', handleNumEntry); + } + + for (const item of Object.values(positionDropdownItems)) { + const radioInput = item.querySelector('input'); + radioInput.addEventListener('change', handleRadioEntry); + } +} + +export { initStatEntry }; diff --git a/www/.DS_Store b/www/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..901881d9a08597f6f4f858cdb1de45d89d57beea GIT binary patch literal 6148 zcmeHKyH3ME5S)b+ktj$>d0!BTKQK{JQ1bzx5CljTK|-W<$7jRr1C+>6&`_XRYj^I} zJ9qXJULOEkyl&2c8GtF>5pN#G=J(x4c2pT7(z(VpUhsfN+@T*;f1hyfDW1uA;CCsP>k5a&q{Cuou2VZ( zolq=xXTC)_tV>js0#e{mf#aOcy#KH0Kb-#$Nt#IkDe$ipu*vdjx!@~RZymjy_u59k sp??}{qnsgHF)>;(C*F$B_Uf9CdA} \ No newline at end of file From 42bebbd1b6a39096fc14b218a43cdec01bc8169c Mon Sep 17 00:00:00 2001 From: Anna Duan Date: Thu, 7 Nov 2024 13:51:48 -0500 Subject: [PATCH 3/9] Added 2020, 2022, 2023 data --- .DS_Store | Bin 8196 -> 8196 bytes data/.DS_Store | Bin 6148 -> 6148 bytes data/stats_2020_2024.json | 182 ++++++++++++++++++++++++++++++++++++++ index.html | 44 +++++++++ js/chart_data.js | 2 +- js/firebase.js | 33 +++++++ js/main.js | 3 +- js/stat_entry.js | 10 +-- 8 files changed, 267 insertions(+), 7 deletions(-) create mode 100644 data/stats_2020_2024.json create mode 100644 js/firebase.js diff --git a/.DS_Store b/.DS_Store index 8e09716f56a4927b90859f1e5bb311609c8d2b2a..c8b12e6301143cd0e203c26e1831d53221cf2446 100644 GIT binary patch delta 32 ncmZp1XmQwZN|4{gKu5vM(70Adq1w>M#AxzfVT;XzLZ5j7rCh7yKEATDNzXE0(g0O84Mj7ofrN(>AP>i?l& Oa~NYL+h%r-zx)7Q>=efU delta 32 ocmZoMXfc@J&&V_}VE1GL5thmPj9!};GN!RjZ1C93&heKY0If(0mjD0& diff --git a/data/stats_2020_2024.json b/data/stats_2020_2024.json new file mode 100644 index 0000000..b39cf15 --- /dev/null +++ b/data/stats_2020_2024.json @@ -0,0 +1,182 @@ +{ + "OL": [ + { + "Height": [76, 75, 76, 74.5, 76, 76.5, 78.5, 76, 77, 75.5, 75.5, 77, 72.45, 72.42, 72.24, 72.25, 72.16, 72.31, 72.4, 72.2, 72.2, 72.21, 72.3, 72.2, 76.5, 74.5, 74, 75.25, 75.5, 74, 75.875, 76.5, 74.5, 76, 74.25, 75, 75.5, 75.25, 75.375, 76, 75.5, 74.4, 74.2, 74, 75, 75.25, 75.3, 75.3, 76.6, 74.5], + "Wingspan": [75, 75, 75, 75, 75, 78, 81, 75, 78, 73, 73, 77, 80, 78.5, 78.3, 76.3, 73.8, 76.8, 80.5, 78.3, 75.9, 77.5, 78.1, 77, 77.5, 77.5, 76.5, 77.5, 75.75, 76.125, 79, 78.5, 80.5, 78.125, 77, 77, 77, 74.875, 76.75, 71, 79, 77, 77.25, 76, 75, 75, 79.5, 77.5], + "Weight": [285, 295, 287, 280, 290, 280, 262, 286, 279, 300, 270, 289, 290.4, 272, 286.4, 295.6, 277, 287.6, 287, 281.4, 263.8, 297, 296.8, 289, 253, 304.2, 275.4, 291.8, 267.8, 286.6, 289.2, 283.6, 299, 300, 271, 303.6, 269.6, 283, 289.8, 295, 263, 296, 285, 282, 301.2, 300.6, 265, 300, 288], + "225lb Bench": [17, 22, 16, 14, 23, 20, 16, 11, 11, 24, 20, 3, 30, 13, 23, 10, 14, 16, 14, 13, 24, 26, 22, 25, 31, 15, 12, 15, 12, 16, 16, 14, 30, 16, 15, 12, 15, 15, 28, 15, 35, 32, 17, 14, 15, 18, 27], + "Bench": [315, 365, 315, 305, 405, 365, 315, 310, 275, 385, 345, 255, 425, 305, 375, 310, 340, 300, 315, 300, 395, 405, 360, 395, 425, 315, 305, 325, 325, 315, 330, 315, 305, 395, 335, 415, 395, 335, 300, 355, 365], + "Squat": [415, 625, 415, 365, 555, 450, 455, 385, 550, 405, 365, 310, 485, 515, 365, 445, 385, 500, 385, 515, 515, 500, 565, 605, 565, 335, 415, 510, 545, 425, 545, 505, 525, 475, 615, 545, 545, 505, 510, 515], + "Vertical Jump": [23.6, 25.9, 25.1, 21.1, 26.8, 28.4, 26.3, 23, 23.5, 17.4, 18, 30.9, 24.8, 26.1, 26.4, 22.4, 23.4, 26.1, 24.4, 29.8, 27, 30.8, 23, 30.3, 27.8, 30, 24.2, 26, 22.4, 24.2, 26.5, 25.4, 24.1, 26, 32.4, 30.8, 22.5, 30, 20.5, 23.2, 22, 23.8, 24.6, 24.4, 29], + "Broad Jump": [88.5, 87, 93, 87, 96, 103, 98, 97, 95, 88, 77, 104, 91, 97, 85, 92, 99, 99, 88, 106, 105, 87, 97.5, 98, 105, 92.5, 95, 90.25, 98, 96.25, 108, 93, 99, 103, 103], + "10-Yard Sprint": [1.8, 1.74, 1.65, 1.7, 1.85, 1.63, 1.83, 1.83, 1.81, 1.91, 1.93, 1.86, 1.92, 1.92, 2.01, 2.03, 1.99, 1.78, 1.81, 1.75, 1.71, 1.83, 1.6, 1.68, 1.6, 1.68, 1.67, 1.78, 2.01, 1.65, 1.66, 1.69, 1.78, 1.92, 2.01, 1.62, 1.91, 1.8, 1.74, 1.76, 1.85, 1.97, 1.86, 1.83, 1.77], + "Year": [2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024], + "Hang Clean": [295, 265, 265, 225, 225, 255, 305, 255, 265], + "L Drill": [7.75, 7.91, 8.25, 7.96, 8.28, 7.9, 7.91, 8.12, 7.81, 7.98, 7.78, 7.59, 7.61, 8.07, 7.97, 8.16], + "Pro Agility": [4.7, 4.65, 5.04, 5.22, 5.19, 4.78, 5, 4.82, 4.67, 5.03, 4.99, 4.82, 5.05, 4.84, 5.06, 4.8], + "Power Clean": [255, 295, 245, 280, 315, 305, 285, 245, 305], + "Flying 10": [1.19, 1.19, 1.13, 1.27, 1.25, 1.21, 1.16, 1.19], + "60-Yard Shuttle": [13.62, 14.03, 13.21, 14.47, 13.38, 13.9, 12.97] + } + ], + "QB": [ + { + "Height": [74, 73, 75.5, 76, 72.17, 72.1, 72.17, 60.17, 72.05, 72, 77.25, 76, 74, 73, 74, 76, 72, 78, 70.2], + "Wingspan": [74, 76, 77, 74, 74.8, 74.3, 74.3, 72.8, 71.5, 73, 79, 76.875, 75, 72, 75, 75.5, 74, 79.5, 72], + "Weight": [205, 209, 195, 201, 204.6, 205, 184, 187.4, 194.8, 204.6, 221, 192.6, 201.8, 195, 212, 202, 196, 228.6, 185], + "225lb Bench": [6, 12, 0, 4, 2, 8, 7, 6, 0, 3, 3, 1, 5, 2, 15, 3], + "Bench": [280, 300, 215, 240, 255, 280, 205, 260, 205, 265, 245, 225, 260, 235], + "Squat": [455, 415, 315, 350, 315, 425, 250, 425, 340, 425, 355, 395, 355, 355], + "Vertical Jump": [28, 35, 26.5, 27.1, 29, 27.1, 27, 31.5, 26.1, 32.3, 24.3, 35.2, 27, 28, 25.2, 29.3, 25.2, 27.5], + "Broad Jump": [96, 114, 99, 102, 97, 97, 94, 112, 94, 108, 104, 96.5, 100.5, 115, 107], + "10-Yard Sprint": [1.59, 1.63, 1.78, 1.67, 1.73, 1.59, 1.81, 1.56, 1.81, 1.59, 1.9, 1.68, 1.57, 1.63, 1.68, 1.62, 1.7, 1.66], + "Year": [2020, 2020, 2020, 2020, 2022, 2022, 2022, 2022, 2022, 2023, 2023, 2023, 2023, 2023, 2024, 2024, 2024, 2024, 2024], + "Hang Clean": [285, 210, 215], + "L Drill": [7.09, 7.31, 7.5, 6.92, 7.27, 7.35, 7.01], + "Pro Agility": [4.51, 4.53, 4.87, 4.55, 4.7, 4.83, 4.55], + "Power Clean": [225, 225], + "Flying 10": [1.06, 1.03, 1.07, 1.04], + "60-Yard Shuttle": [12.72, 12.6, 13.9, 12.56] + } + ], + "RB": [ + { + "Height": [70, 70, 65, 72, 71, 60.81, 60.8, 60.85, 61.13, 60.73, 60.9, 60.9, 67.125, 70, 69.125, 69, 67, 67, 69, 69.7, 68, 67.25, 69.5, 69.1], + "Wingspan": [68, 69, 63, 72, 67, 68.3, 71.3, 71.8, 73, 71.3, 70.5, 71.3, 70.5, 72.25, 72.625, 69.75, 70.5, 70, 72.5, 69, 70.5, 71, 73.5, 73], + "Weight": [185, 215, 174, 205, 197, 212.8, 198.2, 189.8, 207.4, 186.6, 222.2, 184.4, 180.2, 195, 189.6, 208.4, 201, 187.2, 195.4, 186.6, 213, 205, 190, 196, 196], + "225lb Bench": [6, 11, 17, 12, 2, 16, 15, 17, 15, 7, 6, 15, 9, 19, 19, 14, 8, 23, 18, 10, 14, 12], + "Bench": [255, 315, 350, 300, 245, 345, 315, 340, 335, 270, 310, 340, 255, 340, 310, 355, 320, 305, 315, 285], + "Squat": [405, 500, 405, 375, 565, 485, 445, 475, 405, 455, 605, 395, 455, 520, 605, 495, 430, 475, 425], + "Vertical Jump": [28.5, 29, 27.9, 29.2, 32.9, 30.9, 29.9, 33.3, 32.9, 35.1, 29, 35.3, 36, 31.2, 32.7, 38.6, 30, 42.7, 34.7, 35.4, 35, 29.6], + "Broad Jump": [103, 104, 107, 111, 105, 105, 108, 105, 109, 103.5, 105, 111.5, 112.5, 112, 111, 112.5, 114, 113], + "10-Yard Sprint": [1.53, 1.61, 1.54, 1.59, 1.55, 1.6, 1.57, 1.6, 1.53, 1.47, 1.57, 1.43, 1.61, 1.56, 1.74, 1.58, 1.6, 1.64, 1.7, 1.68, 1.59, 1.6], + "Year": [2020, 2020, 2020, 2020, 2020, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2024, 2024, 2024, 2024, 2024], + "Hang Clean": [275, 225, 235, 245], + "L Drill": [6.25, 7.68, 7.03, 7.84, 7.18, 7.34, 7.49, 7.41, 7.01], + "Pro Agility": [4.63, 4.49, 4.25, 4.25, 4.39, 4.86, 4.53, 4.8, 4.66], + "Power Clean": [315, 295, 275, 275, 235], + "Flying 10": [1.02, 1.04, 1.03, 1.01, 1], + "60-Yard Shuttle": [12.5, 13.25, 12.7, 12.6, 12.88] + } + ], + "TE": [ + { + "Height": [76.5, 75.5, 76, 78, 73.5, 72, 75.3, 72.44, 72.31, 72.24, 72.3, 72.21, 72.26, 72.25, 72.3, 61.15, 74, 75, 74.75, 77, 74.7, 77, 75, 74.2, 76, 74.75, 77], + "Wingspan": [76, 79, 75, 77, 72, 73, 76, 75.8, 77.1, 75.5, 78.8, 75.3, 80.8, 77.5, 76.3, 75, 78.875, 78.5, 75.5, 78, 76, 77, 75.5, 79, 78.5, 75, 77], + "Weight": [235, 235, 241, 225, 228, 226, 224.6, 242.8, 222, 236.8, 241.2, 242, 235, 239, 217.2, 248.6, 249.2, 235.6, 227, 218.8, 232, 226, 243, 240, 237, 238], + "225lb Bench": [6, 5, 13, 7, 15, 12, 7, 11, 6, 11, 10, 11, 14, 15, 16, 6, 13, 7, 7, 7, 7, 14, 15], + "Bench": [285, 265, 270, 335, 275, 315, 305, 260, 320, 255, 280, 305, 295, 305, 325, 315, 285, 315, 290, 265, 260, 275, 275, 275, 275, 305], + "Squat": [375, 315, 385, 405, 510, 425, 405, 345, 455, 405, 385, 360, 445, 425, 415, 385, 425, 365, 455, 405], + "Vertical Jump": [27.2, 29.3, 28.6, 29.6, 29.3, 29.1, 28.4, 30.4, 24.8, 25.3, 27.4, 34.9, 30.9, 25.4, 31, 27.6, 30.5, 28.1, 30.1, 25.8, 25.4, 29.3], + "Broad Jump": [104, 111, 102, 110, 100, 112, 102, 108, 102, 100, 110, 113, 112, 99, 108.25, 114.25, 110, 109, 101], + "10-Yard Sprint": [1.67, 1.59, 1.76, 1.64, 1.73, 1.61, 1.72, 1.7, 1.68, 1.72, 1.65, 1.62, 1.68, 1.87, 1.51, 1.58, 1.7, 1.66, 1.76, 1.68], + "Year": [2020, 2020, 2020, 2020, 2020, 2020, 2020, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2023, 2023, 2023, 2023, 2023, 2023, 2024, 2024, 2024, 2024, 2024], + "Hang Clean": [255, 225, 225, 245], + "L Drill": [7.62, 7.22, 7.38, 7.3, 8.14, 7.33, 7.15], + "Pro Agility": [4.74, 4.72, 4.64, 4.76, 5.1, 4.79, 4.68], + "Power Clean": [275, 275, 265], + "Flying 10": [1.06, 1.07, 1.1], + "60-Yard Shuttle": [12.94, 13.38, 13.34, 12.75] + } + ], + "WR": [ + { + "Height": [72.5, 72, 73.5, 76, 75, 70, 74, 73, 73, 71, 72.22, 60.93, 72, 61.1, 60.6, 72.27, 72.02, 72.3, 69.5, 68.75, 72.25, 73, 75.5, 71, 75.5, 74.5, 74, 69, 71.875, 75.5, 71.5, 73.73, 75.5, 71.7, 72.25, 74.1, 68.7, 69, 71.7, 66.3, 71.5], + "Wingspan": [72, 73, 71, 73, 75, 68, 74, 72, 74, 72, 76, 70.5, 72.8, 72.5, 69.8, 74.5, 77, 74, 0, 72.25, 70.875, 73, 75, 77, 74, 79.5, 76.5, 80.5, 70.125, 73, 76, 71, 75, 78, 73, 72.5, 80, 70, 72.5, 69, 71], + "Weight": [196, 181, 191, 198, 187, 194, 196, 186, 208, 184, 202.4, 187, 182, 185.8, 176.4, 195.6, 214, 199, 176, 164, 192.2, 191.8, 195, 189.6, 207.8, 202.6, 215.6, 169.8, 189, 207.6, 179.2, 191, 217, 195, 191, 214.3, 170, 179, 185, 179, 190], + "225lb Bench": [7, 9, 7, 10, 0, 10, 4, 8, 5, 9, 6, 5, 10, 3, 11, 10, 16, 8, 6, 6, 4, 12, 7, 0, 3, 3, 14, 12, 6, 9, 19, 15, 7, 8, 1], + "Bench": [265, 290, 265, 275, 220, 300, 260, 265, 265, 295, 195, 225, 260, 295, 245, 295, 235, 220, 275, 325, 270, 260, 265, 190, 285, 245, 225, 335, 300, 275, 280, 315, 320, 265, 295, 230], + "Squat": [405, 385, 365, 345, 375, 405, 355, 365, 435, 375, 405, 340, 435, 370, 385, 385, 405, 375, 405, 440, 415, 405, 500, 365, 405, 405, 495, 405, 375, 475, 365, 455, 440], + "Vertical Jump": [35.7, 29.9, 36.3, 29.3, 37.7, 32.4, 29, 33.2, 33.2, 33.9, 31.2, 29, 32, 37.1, 34, 30, 30, 32.8, 38, 36.7, 40.5, 38.5, 33.5, 40, 30.1, 36.4, 29, 31.4, 32, 34, 36.4, 38.7, 31.1, 34.7, 34.6, 30.6, 31.5, 35, 28], + "Broad Jump": [112, 115, 113, 111, 121, 105, 97, 110, 118, 114, 9, 103, 9, 116, 110, 105, 107.25, 112.5, 125.75, 122, 112.25, 125, 105.25, 108, 125, 130, 118, 117.5, 124, 111, 105.5, 111.5, 111], + "10-Yard Sprint": [1.57, 1.64, 1.62, 1.61, 1.46, 1.67, 1.63, 1.6, 1.55, 1.65, 1.5, 1.68, 1.62, 1.65, 1.57, 1.5, 1.48, 1.5, 1.48, 1.52, 1.52, 1.57, 1.46, 1.57, 1.56, 1.68, 1.71, 1.68, 1.57, 1.66, 1.59, 1.76, 1.7, 1.63], + "Year": [2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024], + "Hang Clean": [245, 215, 255, 255, 225, 225, 295, 245, 215], + "L Drill": [7.53, 7.15, 6.96, 6.87, 7.34, 6.94, 7, 7.25, 6.95, 6.79, 7.57, 6.92, 7.1, 6.97, 6.98, 7.21, 6.91], + "Pro Agility": [4.39, 4.51, 4.33, 4.25, 4.31, 4.36, 4.49, 4.4, 4.38, 4.42, 4.78, 4.43, 4.29, 4.66, 4.53, 4.65, 4.46], + "Power Clean": [275, 295, 275, 255, 315, 225, 275, 235], + "Flying 10": [1, 1.02, 1.05, 1.03, 0.98, 0.95, 1.08, 0.97, 1.05], + "60-Yard Shuttle": [12.12, 11.81, 13.47, 12.27, 12.53, 12.6, 12.09, 12.38, 12.09] + } + ], + "DB": [ + { + "Height": [73, 75, 74.5, 71.3, 74, 71, 72, 73, 70, 71, 72.11, 61.15, 60.9, 60.9, 61.04, 72.2, 72.12, 72.16, 60.75, 72.14, 60.82, 60.87, 61.11, 60.9, 71.5, 73, 74, 70, 72.875, 69.625, 73.5, 74, 70.125, 71, 71, 69, 69.25, 73.5, 71, 69, 68.25, 70.25, 70, 69.5, 70, 70, 70.25, 73, 74, 71, 72.87, 71.6, 71, 71, 68.4, 69.6], + "Wingspan": [77, 73, 75, 72, 69, 68, 74, 76, 72, 69, 79.5, 75.5, 75, 71.9, 74.8, 73.5, 77.3, 72.8, 70.3, 77.3, 71.8, 71.3, 76.3, 70.5, 73.5, 73, 78, 71.875, 76, 72, 77, 73, 70, 77, 73.625, 72.75, 72.5, 76, 74, 71.5, 71, 76, 75.5, 73, 75.5, 71.5, 76, 73, 76, 72, 76.5, 72, 72.5, 75, 69.5, 72], + "Weight": [193, 192, 192, 185, 188, 177, 206, 191, 182, 169, 196.6, 203.4, 178.2, 183, 195.8, 176.4, 177.8, 184.6, 179.4, 202.8, 191.4, 179, 192.8, 190.8, 194.3, 191, 193, 170.4, 168.2, 170.4, 189.4, 164.4, 178.2, 197.2, 179.8, 192, 178, 193, 191.2, 190, 179.4, 177, 173, 184.8, 179, 178.2, 180, 195, 193, 187, 179, 193, 190, 189, 179, 186], + "225lb Bench": [1, 4, 4, 8, 3, 8, 3, 10, 8, 3, 8, 8, 7, 3, 8, 6, 15, 16, 11, 4, 12, 14, 7, 10, 0, 0, 6, 2, 8, 14, 2, 13, 7, 6, 6, 12, 14, 7, 6, 6, 5, 10, 8, 2, 9, 16, 6, 3, 15, 9], + "Bench": [245, 260, 265, 260, 245, 275, 275, 280, 275, 250, 275, 245, 275, 255, 255, 255, 340, 330, 305, 265, 300, 305, 325, 285, 235, 310, 305, 285, 285, 245, 300, 325, 275, 255, 265, 265, 285, 265, 285, 300, 230, 325, 275, 235, 315, 285], + "Squat": [385, 340, 365, 410, 325, 425, 415, 455, 335, 395, 475, 445, 375, 400, 375, 245, 500, 500, 465, 445, 395, 435, 415, 500, 465, 465, 445, 375, 385, 420, 500, 395, 375, 415, 405, 445, 415, 405, 375, 445, 335, 425, 405, 405, 485, 405], + "Vertical Jump": [31.4, 27.4, 37, 32.8, 29.1, 30.9, 29.5, 33.9, 34, 34.4, 37.6, 31.1, 29.8, 31.2, 31, 34.3, 33.9, 31.4, 34.2, 38, 33, 34.3, 34.6, 40, 33, 35, 31.8, 38.6, 31.7, 37.9, 33.4, 33.1, 34.7, 35, 32, 36.1, 33.5, 36.1, 31.8, 37.9, 34.8, 35.7, 34.5, 30.4, 31.6, 27.3, 29.7, 31.8, 37.1, 31.6, 33.5, 34.1, 31.1], + "Broad Jump": [111, 102, 121, 105, 110, 106, 110, 114, 106, 111, 10.5, 9.1, 9.2, 9.1, 9.2, 9.5, 9, 9, 106, 10, 8.9, 118, 123.5, 122, 114, 110, 114.5, 111, 105, 114.5, 114, 117.5, 109, 119, 119, 114, 113.5, 110, 115, 117, 125.5, 114, 118, 117, 119], + "10-Yard Sprint": [1.65, 1.6, 1.55, 1.57, 1.62, 1.59, 1.6, 1.5, 1.56, 1.67, 1.7, 1.59, 1.54, 1.62, 1.55, 1.75, 1.5, 1.58, 1.45, 1.6, 1.58, 1.49, 1.45, 1.51, 1.53, 1.53, 1.58, 1.59, 1.65, 1.6, 1.53, 1.6, 1.47, 1.54, 1.49, 1.64, 1.74, 1.66, 1.69, 1.76, 1.7, 1.77, 1.63, 1.64, 1.56, 1.66], + "Year": [2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024], + "Hang Clean": [285, 300, 300, 245, 265, 205, 195, 300, 265, 205, 205, 255], + "L Drill": [7.5, 7.16, 7.1, 7.25, 7.47, 7.19, 7.19, 7, 7.53, 7.41, 7.03, 7.4, 7.04, 7.27, 7.15, 7.25, 7.08, 7.02, 7.06, 7.54, 6.87, 6.84, 7.09, 7.16], + "Pro Agility": [4.71, 4.5, 4.03, 4.59, 4.49, 4.45, 4.55, 4.5, 4.53, 4.49, 4.06, 4.71, 4.55, 4.6, 4.6, 4.72, 4.67, 4.53, 4.63, 4.54, 4.5, 4.74, 4.58], + "Power Clean": [255, 265, 235, 225, 255, 265, 205, 305, 225, 235, 285, 245], + "Flying 10": [1.04, 1.03, 1.04, 1, 1.09, 1, 1.08, 1.03, 1.02, 1, 1.01, 1.1], + "60-Yard Shuttle": [12.22, 12.56, 12.59, 12.72, 12.69, 12.09, 12.43, 12.6, 12.04, 12.44, 12, 12.31] + } + ], + "DL": [ + { + "Height": [74, 74, 76, 76, 74, 74.3, 75, 74, 74, 75.3, 77, 73, 76.5, 72.2, 72.34, 72.3, 72.2, 72.27, 72.42, 72.47, 72.34, 72.11, 72.28, 72.47, 72.15, 72.37, 72.26, 72.35, 72.34, 75, 75.5, 74.375, 77.25, 74.875, 75, 73, 75.25, 75, 77, 76.25, 75, 73.75, 76, 76, 74.5, 75, 75, 75.5, 76, 75.25, 75.75, 77, 75.4, 73.5, 76, 74.5, 77.25, 76.3, 73.3, 75.5, 75, 75.5, 76], + "Wingspan": [75, 81, 77, 76, 75, 76, 75, 73, 75, 78, 76, 72, 76, 77.8, 76, 77.8, 80.5, 78.5, 77.8, 78.5, 79.3, 73.5, 77, 78.3, 73.8, 80.5, 80.8, 78.5, 73.3, 80.5, 78, 74.25, 78.5, 75.5, 78.5, 75.25, 77.75, 78.75, 78, 80.5, 79, 75.125, 79, 73.5, 76, 74, 78, 75.125, 77, 79, 77.5, 77.5, 77, 77, 76, 73.5, 78, 79.5, 76.5, 75, 78, 81.5], + "Weight": [261, 271, 230, 243, 203, 275, 241, 260, 241, 270, 230, 272, 274, 249.4, 238.2, 257.2, 252, 251, 230.2, 239.8, 292.4, 278.6, 225, 238.4, 252.8, 260, 235.8, 285.6, 276, 230, 250, 242.8, 239, 245.4, 252.6, 228.8, 231.8, 258.4, 247.2, 250, 233.7, 225.6, 276, 239, 262.4, 227.2, 281.8, 283.8, 291.2, 237.6, 279, 278, 265, 242, 270, 278.5, 271, 263, 229, 262, 249], + "225lb Bench": [4, 21, 16, 22, 6, 12, 14, 8, 19, 17, 14, 19, 10, 14, 8, 22, 15, 22, 15, 22, 9, 15, 15, 20, 3, 22, 19, 3, 17, 8, 13, 15, 16, 15, 15, 22, 17, 20, 14, 4, 21, 9, 16, 16, 10, 16, 16, 25, 15, 7, 20, 17, 23, 16, 17, 15, 5, 15], + "Bench": [285, 355, 330, 330, 285, 285, 315, 275, 340, 350, 300, 365, 285, 330, 280, 345, 305, 365, 335, 365, 295, 340, 305, 250, 345, 335, 235, 335, 315, 340, 325, 390, 320, 335, 315, 345, 275, 320, 340, 315, 355, 345, 325, 275, 265, 345, 345, 295, 238, 355, 265], + "Squat": [395, 620, 500, 530, 405, 450, 435, 500, 475, 455, 440, 415, 455, 505, 405, 475, 500, 540, 505, 525, 515, 520, 505, 385, 500, 475, 405, 475, 475, 460, 485, 405, 585, 405, 545, 425, 495, 450, 495, 585, 405, 515, 525, 500, 455, 595, 465, 415, 345, 385, 585, 425, 455, 425, 455, 505], + "Vertical Jump": [27.9, 38, 36.9, 34.9, 30.4, 28.5, 29.5, 25.9, 30.6, 23.4, 27.4, 22.4, 29, 36.9, 29.6, 27.8, 27.5, 32.8, 26.5, 33.1, 28.1, 25.2, 26.5, 36, 27, 25, 32, 30.6, 31, 23.1, 26.8, 26.8, 31.9, 33, 32.7, 33.9, 30.9, 37.3, 32.8, 30.1, 25.8, 33.6, 30.6, 29.4, 34.5, 22.4, 31, 26.5, 32.2, 28.3, 28.9, 23.8, 29.9, 20.3, 27.6, 24.4, 35.2, 36.5, 29.6], + "Broad Jump": [107, 120, 121, 111, 116, 97, 103, 96, 108, 99, 101, 93, 104, 9, 109, 103, 98, 117, 106, 116, 101, 98, 100, 122, 102, 100, 110, 108, 108, 102, 109.5, 116, 111.5, 110, 123, 106, 112, 105, 106, 105, 109, 121, 110.5, 109.5, 107, 95, 107, 99.5, 123, 124, 105, 112.5], + "10-Yard Sprint": [1.6, 1.52, 1.6, 1.64, 1.57, 1.78, 1.58, 1.74, 1.56, 1.75, 1.7, 1.72, 1.63, 1.62, 1.7, 1.81, 1.85, 1.79, 1.75, 1.79, 1.85, 1.83, 1.65, 1.73, 1.79, 1.82, 1.59, 1.67, 1.62, 1.59, 1.69, 1.59, 1.59, 1.6, 1.55, 1.65, 1.72, 1.61, 1.63, 1.9, 1.62, 1.66, 1.68, 1.74, 1.77, 1.84, 1.75, 1.87, 1.78, 1.75, 1.63, 1.74, 1.73, 1.64], + "Year": [2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024], + "Hang Clean": [225, 275, 245, 295, 265, 295, 285, 315, 245, 355, 280, 255, 265, 335], + "L Drill": [7.53, 7.22, 7.5, 7.34, 7.31, 7.16, 7.38, 7.69, 7.56, 7.97, 7.32, 7.68, 7.65, 7.48, 7.39, 7.36, 7.13, 7.51, 7.5, 7.51, 7.77, 7.32, 7.49, 7.37, 7.22], + "Pro Agility": [4.69, 4.5, 4.78, 4.72, 4.4, 4.28, 4.57, 4.63, 4.72, 4.95, 4.63, 4.74, 4.79, 4.66, 4.93, 4.7, 4.84, 4.87, 4.97, 4.83, 5.13, 4.67, 4.66, 4.76], + "Power Clean": [305, 295, 325, 260, 285, 275, 305, 275, 305, 285], + "Flying 10": [1.1, 1.09, 1.04, 1.21, 1.09, 1.2, 1.13, 1.16, 1.04, 1.08, 1.15, 1.05], + "60-Yard Shuttle": [12.62, 13.09, 13.06, 13.41, 13.25, 13.79, 13.82, 13.79, 12.47, 13.09, 13.06] + } + ], + "LB": [ + { + "Height": [72, 73.3, 71.5, 72, 73, 73.5, 73, 73, 74, 72, 75.5, 61.05, 72.02, 72.33, 61.17, 60.95, 72.04, 72.1, 72.2, 61.17, 61.12, 61.15, 71.125, 72.875, 75, 73, 73.375, 70, 73, 72, 72, 72, 73.5, 73.5, 72, 74.5, 72, 71, 73.3, 75.75, 74.5, 73.5, 73, 72, 72, 73.5, 72.87], + "Wingspan": [69, 75, 74, 69, 72, 72, 73, 72, 72, 72, 74, 73, 72.8, 72.5, 76.3, 73.1, 72.5, 72.5, 73.3, 76.9, 72.8, 74.3, 75.8, 73.875, 73.5, 77, 71, 79, 73, 75, 76, 73, 72, 74, 74, 76.5, 75, 74.5, 71.5, 77, 76, 79.5, 73, 74.5, 75.5, 74, 74], + "Weight": [208, 220, 215, 212, 230, 211, 222, 208, 235, 202, 235, 236, 202.2, 202.8, 230.2, 223, 215.4, 220, 200.4, 223.4, 229.2, 213.2, 227, 239.2, 215.8, 218, 223.3, 193.6, 218.6, 203, 211, 221, 239, 212.6, 217.4, 213.2, 224, 213, 240, 224, 222, 225, 200, 215, 212, 225, 219.3, 226], + "225lb Bench": [16, 15, 10, 15, 13, 16, 9, 21, 10, 11, 8, 14, 16, 12, 15, 19, 17, 11, 5, 19, 18, 14, 14, 8, 12, 21, 0, 19, 13, 17, 20, 21, 19, 18, 6, 9, 9, 19, 21, 16, 12, 5, 15, 15, 22, 21, 12], + "Bench": [350, 245, 300, 310, 320, 325, 340, 285, 365, 285, 295, 265, 330, 335, 295, 315, 355, 335, 285, 265, 350, 315, 325, 295, 335, 360, 315, 325, 345, 385, 355, 325, 295, 275, 345, 315, 325, 300, 265, 325, 345, 355, 295], + "Squat": [455, 460, 445, 365, 475, 450, 500, 435, 485, 435, 400, 375, 455, 440, 395, 455, 520, 415, 390, 395, 570, 455, 475, 445, 425, 565, 515, 495, 515, 585, 455, 465, 475, 435, 475, 455, 475, 385, 495, 500, 500, 425], + "Vertical Jump": [35.7, 24.6, 30.8, 27.1, 28.8, 32.8, 33.2, 34.1, 33.6, 29.6, 29.4, 28.7, 32, 33, 30.5, 35, 33, 30.4, 32.4, 29.4, 32, 30.9, 29.9, 33.4, 31.9, 35.7, 30.6, 28.6, 35.6, 35.3, 32, 32.3, 32.8, 35.9, 33.7, 28.4, 31.9, 27.3, 29.3, 36.3, 31.7, 31.7, 31.3, 32.1, 35, 27.6], + "Broad Jump": [107, 103, 104, 106, 99, 113, 112, 114, 110, 101, 104, 104, 112, 109, 9, 116, 109, 115, 115, 108, 107, 102, 104, 111, 109, 109, 115.5, 113, 103.75, 109.5, 110, 106, 111, 104, 118.5, 118.5, 112, 121, 109.5, 114, 107], + "10-Yard Sprint": [1.67, 1.61, 1.6, 1.68, 1.7, 1.6, 1.63, 1.57, 1.55, 1.67, 1.62, 1.7, 1.6, 1.57, 1.82, 1.54, 1.51, 1.59, 1.56, 1.66, 1.74, 1.77, 1.64, 1.8, 1.58, 1.64, 1.51, 1.55, 1.63, 1.57, 1.65, 1.6, 1.44, 1.47, 1.62, 1.71, 1.85, 1.64, 1.6, 1.64, 1.66, 1.6, 1.6, 1.72], + "Year": [2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2020, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2023, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024, 2024], + "Hang Clean": [245, 295, 245, 225, 275, 275, 265, 285, 235], + "L Drill": [7.28, 7.16, 7.47, 7.31, 7.46, 7.34, 7.38, 7.29, 7.53, 7.44, 7.39, 7.17, 7.01, 7.16, 7.43, 7.33, 7.4], + "Pro Agility": [4.5, 4.25, 4.55, 4.65, 4.3, 4.61, 4.58, 4.48, 4.71, 4.48, 4.55, 4.82, 4.63, 4.85, 4.54, 4.76, 4.56, 4.49, 4.77, 4.63], + "Power Clean": [225, 300, 265, 285, 255, 275, 315, 305, 265], + "Flying 10": [1, 1.06, 1.03, 1, 1.03, 1.05, 1, 1.03, 1.1], + "60-Yard Shuttle": [12.63, 12.81, 12.87, 11.87, 12.25, 12.97, 12.35, 12.97, 12.85, 12.4] + } + ], + "SP": [ + { + "Height": [75, 74, 74, 72.8, 73.5, 72.14, 61.04, 61.07, 0, 71, 74.5, 72.5, 69.5, 75, 74.5, 76, 72.2, 69.2, 75], + "Wingspan": [74, 79, 74, 72, 73, 77.3, 69.8, 70.3, 0, 71, 77.25, 73, 70, 76.5, 77, 76, 73, 70, 76.5], + "Weight": [197, 191, 197, 214, 189, 198.6, 158.6, 219.6, 162, 199.4, 189.2, 184.6, 204.6, 201, 204, 202, 185, 195], + "225lb Bench": [1, 4, 1, 21, 4, 8, 1, 1, 4, 7, 1, 1, 1, 11, 1, 1, 1, 1], + "Bench": [230, 245, 225, 345, 260, 285, 225, 235, 240, 265, 220, 220, 225, 295, 225, 235, 245, 240], + "Squat": [315, 315, 335, 305, 365, 305, 315, 315, 365, 315, 365, 335, 385, 335, 325, 395, 335], + "Vertical Jump": [29, 30.2, 28, 29.8, 28.9, 30.5, 21, 27.6, 34.9, 33.3, 28.1, 27.7, 31, 26.3, 26.3, 26.3, 29.4], + "Broad Jump": [102, 103, 102, 106, 97, 105, 84, 109.5, 110, 97.5, 96.5, 116, 97, 101, 105], + "10-Yard Sprint": [1.62, 1.65, 1.64, 1.59, 1.78, 1.57, 1.91, 1.58, 1.53, 1.64, 1.62, 1.64, 1.71, 1.78, 1.72, 1.82], + "Year": [2020, 2020, 2020, 2020, 2020, 2022, 2022, 2022, 2023, 2023, 2023, 2023, 2023, 2023, 2024, 2024, 2024, 2024, 2024], + "Hang Clean": [185, 225, 205, 185, 205], + "L Drill": [7.12, 7.41, 7.75, 7.53, 7.1, 7.35, 7.64, 7.71, 7.44, 7.27], + "Pro Agility": [4.55, 4.56, 4.65, 4.85, 4.75, 4.46, 5.01, 4.77, 4.79, 4.67], + "Power Clean": [270, 225, 215, 195, 240], + "Flying 10": [1.07, 1.1, 1.15, 1.08, 1.05], + "60-Yard Shuttle": [12.66, 14.44, 13.5, 12.97, 13.19] + } + ] +} diff --git a/index.html b/index.html index a2c9371..e39d7d9 100644 --- a/index.html +++ b/index.html @@ -53,6 +53,50 @@

Select a position group to start

+ + diff --git a/js/chart_data.js b/js/chart_data.js index 07e2e55..a46c394 100644 --- a/js/chart_data.js +++ b/js/chart_data.js @@ -7,7 +7,7 @@ function calculateChartData(indivStats, events) { let playerPercentiles = []; const inversePercentileStats = [ - 'Flying 10', '10-Yard Dash', '60-Yard Shuttle', 'L Drill', 'Pro Agility' + 'Flying 10', '10-Yard Sprint', '60-Yard Shuttle', 'L Drill', 'Pro Agility' ]; function getPercentiles() { diff --git a/js/firebase.js b/js/firebase.js new file mode 100644 index 0000000..b68113d --- /dev/null +++ b/js/firebase.js @@ -0,0 +1,33 @@ +import {initializeApp} from 'http://www.gstatic.com/firebasejs/11.0.1/firebase-app.js'; +import {getFirestore} from 'http://www.gstatic.com/firebasejs/11.0.1/firebase-firestore.js'; +import {getAnalytics} from 'http://www.gstatic.com/firebasejs/11.0.1/firebase-analytics.js'; +//need to add sdks from firebase products i end up using + + +// For Firebase JS SDK v7.20.0 and later, measurementId is optional +const firebaseConfig = { + apiKey: "AIzaSyAPujgD3xoa9zRjqve5nat6m4Q5Aa6MjDM", + authDomain: "penn-football-benchmarking.firebaseapp.com", + projectId: "penn-football-benchmarking", + storageBucket: "penn-football-benchmarking.firebasestorage.app", + messagingSenderId: "144027601930", + appId: "1:144027601930:web:a4ea4a588776b2341d63f3", + measurementId: "G-834Q3624G1" + }; + +const app = initializeApp(firebaseConfig); +const analytics = getAnalytics(app); +const db = getFirestore(app); + +window.db = db; +window.collection = collection; +window.addDoc = addDoc; +window.getDocs = getDocs; + +async function getRecruitReports() { + const reportsColl = collection(db, 'recruitReports'); + const reports = await getDocs(reportsColl); + return reports; +} + +export { app, analytics, db, getStationReports }; \ No newline at end of file diff --git a/js/main.js b/js/main.js index 7b9d9f7..f5d51fe 100644 --- a/js/main.js +++ b/js/main.js @@ -1,9 +1,10 @@ import { initChart } from './barchart.js'; import { initStatEntry } from './stat_entry.js'; import { calculateChartData } from './chart_data.js'; +//import { getStationReports } from './firebase.js'; // Fetch data -const indivStatsResponse = await fetch('data/stats_2024.json'); +const indivStatsResponse = await fetch('data/stats_2020_2024.json'); const indivStats = await indivStatsResponse.json(); const events = new EventTarget(); diff --git a/js/stat_entry.js b/js/stat_entry.js index e47bbf5..ccc7819 100644 --- a/js/stat_entry.js +++ b/js/stat_entry.js @@ -9,16 +9,16 @@ function initStatEntry(statListEl, positionRadioEl, stats, positions, events) { }); const orderedStats = [ - 'Bench', 'Squat', 'Power Clean', '225lb Bench', - '10-Yard Dash', 'Vertical Jump (vertec)', 'Vertical Jump (mat)', 'Broad Jump', '60-Yard Shuttle', 'L Drill', 'Pro Agility', 'Flying 10', + 'Bench', 'Squat', 'Power Clean', 'Hang Clean', '225lb Bench', + '10-Yard Sprint', 'Vertical Jump', 'Broad Jump', '60-Yard Shuttle', 'L Drill', 'Pro Agility', 'Flying 10', 'Height', 'Weight', 'Wingspan' ]; const unitMapping = { - pounds: ['Bench', 'Squat', 'Power Clean', 'Weight'], + pounds: ['Bench', 'Squat', 'Power Clean', 'Hang Clean', 'Weight'], reps: ['225lb Bench'], - inches: ['Vertical Jump (vertec)', 'Vertical Jump (mat)', 'Broad Jump', 'Height', 'Wingspan'], - seconds: ['10-Yard Dash', '60-Yard Shuttle', 'L Drill', 'Pro Agility', 'Flying 10'] + inches: ['Vertical Jump', 'Broad Jump', 'Height', 'Wingspan'], + seconds: ['10-Yard Sprint', '60-Yard Shuttle', 'L Drill', 'Pro Agility', 'Flying 10'] }; function getUnit(stat) { From fc6af0a7a2d722e95b6927157150c6238030e57d Mon Sep 17 00:00:00 2001 From: Anna Duan Date: Fri, 8 Nov 2024 10:38:39 -0500 Subject: [PATCH 4/9] prep to switch to billboard --- js/barchart.js | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/js/barchart.js b/js/barchart.js index 8ded83d..61013af 100644 --- a/js/barchart.js +++ b/js/barchart.js @@ -1,4 +1,5 @@ import { Chart } from 'https://cdn.jsdelivr.net/npm/chart.js@4.4.4/auto/+esm'; +import { bb } from 'https://cdn.jsdelivr.net/npm/billboard.js@3.14.0/+esm'; const chartInstances = {}; @@ -38,25 +39,34 @@ function initChart(chartEl, positionMedians, statNames, playerStats, playerPerce generateLabels: function (chart) { return [ { - text: '75-100th Percentile', + text: '80+ Percentile', fillStyle: 'rgba(0, 139, 139, 0.5)', - strokeStyle: 'rgba(0, 139, 139, 1)', - lineWidth: 1, - hidden: false + strokeStyle: 'rgba(0, 139, 139, 0.5)', + lineWidth: 1 }, { - text: '50-75th Percentile', + text: '60-79 Percentile', + fillStyle: 'rgba(60, 179, 113, 0.5)', + strokeStyle: 'rgba(60, 179, 113, 0.5)', + lineWidth: 1 + }, + { + text: '40-59 Percentile', fillStyle: 'rgba(255, 215, 0, 0.5)', - strokeStyle: 'rgba(255, 215, 0, 1)', - lineWidth: 1, - hidden: false + strokeStyle: 'rgba(255, 215, 0, 0.5)', + lineWidth: 1 }, { - text: '0-50th Percentile', + text: '20-39 Percentile', fillStyle: 'rgba(250, 128, 114, 0.5)', - strokeStyle: 'rgba(250, 128, 114, 1)', - lineWidth: 1, - hidden: false + strokeStyle: 'rgba(250, 128, 114, 0.5)', + lineWidth: 1 + }, + { + text: '0-19 Percentile', + fillStyle: 'rgba(220, 20, 60, 0.5)', + strokeStyle: 'rgba(220, 20, 60, 0.5)', + lineWidth: 1 } ]; } @@ -108,12 +118,16 @@ function initChart(chartEl, positionMedians, statNames, playerStats, playerPerce function getColor() { return playerPercentiles.map(percentile => { - if (percentile >= 75) { + if (percentile >= 80) { return 'rgba(0, 139, 139, 0.5)'; - } else if (percentile >= 50) { + } else if (percentile >= 60) { + return 'rgba(60, 179, 113, 0.5)'; + } else if (percentile >= 40) { return 'rgba(255, 215, 0, 0.5)'; - } else { + } else if (percentile >= 20) { return 'rgba(250, 128, 114, 0.5)'; + } else { + return 'rgba(220, 20, 60, 0.5)'; } }); } From 87a7dc0c95761041af7e9ec618783353686de2ca Mon Sep 17 00:00:00 2001 From: Anna Duan Date: Fri, 8 Nov 2024 18:11:25 -0500 Subject: [PATCH 5/9] Fixed chart clearing issue --- css/styles.css | 32 +++++--- index.html | 62 ++++---------- js/barchart.js | 205 +++++++++++++++++++++-------------------------- js/chart_data.js | 5 +- js/main.js | 18 ++++- js/radar.js | 46 +++++++++++ js/stat_entry.js | 11 ++- 7 files changed, 199 insertions(+), 180 deletions(-) create mode 100644 js/radar.js diff --git a/css/styles.css b/css/styles.css index 3d51276..7acb3d0 100644 --- a/css/styles.css +++ b/css/styles.css @@ -4,7 +4,7 @@ body { } #page-name { - font-size: 3rem; + font-size: 2rem; font-weight: 100; color: #011f5b; margin-left: 1rem; @@ -16,21 +16,21 @@ body { align-items: center; justify-content: flex-start; color: #011f5b; - font-size: 0.6rem; } .header img { margin-left: 1rem; - width: 4rem; + margin-right: 1rem; + width: 3rem; align-self: center; } .benchmark-page { display: grid; grid-template-columns: repeat(6, 1fr); - grid-template-rows: repeat(4, min-content); - gap: 40px; + grid-template-rows: repeat(4, 1fr); align-content: flex-start; + gap: 1rem; } .benchmark-menu { @@ -38,7 +38,6 @@ body { } fieldset { - width: 100%; height: auto; text-align: left; text-justify: auto; @@ -58,7 +57,7 @@ fieldset { } select { - width: 100%; + width: 60%; padding: 0.5rem; font-size: 1rem; font-weight: 100; @@ -103,15 +102,26 @@ input[type="number"] { } .benchmark-chart-area { + display: grid; + grid-column: 2 / 6; + grid-row: 1 / 4; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: repeat(2, 1fr); + height: 70vh; + gap: 1.5rem; +} + +.pct-chart { display: flex; flex-direction: column; - grid-column: 2 / 7; - grid-row: 1 / 4; - justify-content: flex-start; + align-items: center; + justify-content: center; + border-radius: 1rem; + background-color: white; + box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1); } #position-title { - margin-left: 5rem; font-size: 2rem; font-weight: 100; color: #011f5b; diff --git a/index.html b/index.html index e39d7d9..f1ff3b6 100644 --- a/index.html +++ b/index.html @@ -5,13 +5,14 @@ Penn Football Strength Benchmarking +
@@ -45,58 +46,23 @@
-

Select a position group to start

-
- -
+
+
+
+
+
+
- + + + + + + diff --git a/js/barchart.js b/js/barchart.js index 61013af..72ad7a1 100644 --- a/js/barchart.js +++ b/js/barchart.js @@ -1,136 +1,115 @@ -import { Chart } from 'https://cdn.jsdelivr.net/npm/chart.js@4.4.4/auto/+esm'; -import { bb } from 'https://cdn.jsdelivr.net/npm/billboard.js@3.14.0/+esm'; +const barInstances = {}; -const chartInstances = {}; +function initBar(barEl, positionMedians, statNames, playerStats, playerPercentiles) { -function initChart(chartEl, positionMedians, statNames, playerStats, playerPercentiles) { + const statCategories = { + speed: ['10-Yard Sprint', 'Flying 10'], + agility: ['Pro Agility', 'L Drill', '60-Yard Shuttle'], + power: ['Vertical Jump', 'Broad Jump', 'Hang Clean', 'Power Clean'], + strength: ['Squat', 'Bench', '225lb Bench'], + anthro: ['Weight', 'Height', 'Wingspan'] + }; - if (chartInstances[chartEl.id]) { - chartInstances[chartEl.id].destroy(); - } + const category = barEl.id.replace('-chart', '').toLowerCase(); + const metrics = statCategories[category]; - const data = { - labels: statNames, - datasets: [ - { - label: 'Player Percentiles', - data: playerPercentiles, - backgroundColor: getColor(), - borderColor: getColor(), - borderWidth: 1, - barThickness: 'flex', - maxBarThickness: 70, - borderRadius: 10, - borderSkipped: false, - minBarLength: 10 - } - ] - }; + const filteredStatNames = []; + const filteredPositionMedians = []; + const filteredPlayerStats = []; + const filteredPlayerPercentiles = []; - const options = { - plugins: { - title: { - display: true, - text: "Athlete Percentiles", - }, - legend: { - display: true, - labels: { - generateLabels: function (chart) { - return [ - { - text: '80+ Percentile', - fillStyle: 'rgba(0, 139, 139, 0.5)', - strokeStyle: 'rgba(0, 139, 139, 0.5)', - lineWidth: 1 - }, - { - text: '60-79 Percentile', - fillStyle: 'rgba(60, 179, 113, 0.5)', - strokeStyle: 'rgba(60, 179, 113, 0.5)', - lineWidth: 1 - }, - { - text: '40-59 Percentile', - fillStyle: 'rgba(255, 215, 0, 0.5)', - strokeStyle: 'rgba(255, 215, 0, 0.5)', - lineWidth: 1 - }, - { - text: '20-39 Percentile', - fillStyle: 'rgba(250, 128, 114, 0.5)', - strokeStyle: 'rgba(250, 128, 114, 0.5)', - lineWidth: 1 - }, - { - text: '0-19 Percentile', - fillStyle: 'rgba(220, 20, 60, 0.5)', - strokeStyle: 'rgba(220, 20, 60, 0.5)', - lineWidth: 1 - } - ]; - } - } - }, - tooltip: { - usePointStyle: true, - callbacks: { - label: function (context) { - const index = context.dataIndex; - const statName = context.label; - const playerStat = playerStats[index]; - const positionMedian = positionMedians[index]; - const percentile = playerPercentiles[index]; + statNames.forEach((statName, index) => { + if (metrics.includes(statName)) { + filteredStatNames.push(statName); + filteredPositionMedians.push(positionMedians[index]); + filteredPlayerStats.push(playerStats[index]); + filteredPlayerPercentiles.push(playerPercentiles[index]); + } + }); - return [ - `${statName}`, - `Player Stat: ${playerStat}`, - `Position Median: ${positionMedian}`, - `Percentile: ${percentile}%` - ]; - }, - labelPointStyle: function (context) { - return { - pointStyle: 'star' - }; - } + let columns = [ + ['x', ...filteredStatNames], + ['Player Percentiles', ...filteredPlayerPercentiles] + ]; + + if (barInstances[barEl.id]) { + console.log(`Destroying chart with id: ${barEl.id}`); + barInstances[barEl.id].destroy(); + } + + const colors = getColor(filteredPlayerPercentiles); + + barInstances[barEl.id] = bb.generate({ + title: { + text: category.charAt(0).toUpperCase() + category.slice(1).toUpperCase() + }, + data: { + x: 'x', + columns: columns, + type: 'bar', + color: function (color, d) { + if (d && d.index !== undefined) { + return colors[d.index]; } + return color; } }, - indexAxis: 'y', - aspectRatio: 2, - scales: { - x: { - beginAtZero: true, - min: 0, - max: 100 + transition: { + duration: 100 + }, + bar: { + padding: 1, + radius: { + ratio: 0.2 + }, + width: { + ratio: 0.5, + max: 50, } }, - elements: { - bar: { - minBarLength: 40 + axis: { + rotated: false, + x: {show: true, + type: 'category', + categories: filteredStatNames, + }, + y: { + show: false, + max: 100, + padding: { + top: 0, + bottom: 0 + }, + tick: { + show: true, + text: { + show: false + } + } } }, - responsive: true - }; - - chartInstances[chartEl.id] = new Chart(chartEl, { type: 'bar', data, options }); + legend: { + show: false + }, + bindto: barEl + }); - function getColor() { - return playerPercentiles.map(percentile => { + function getColor(percentiles) { + return percentiles.map(percentile => { if (percentile >= 80) { - return 'rgba(0, 139, 139, 0.5)'; + // darkcyan + return 'rgba(0, 139, 139, 0.7)'; } else if (percentile >= 60) { - return 'rgba(60, 179, 113, 0.5)'; + // gold + return 'rgba(255, 215, 0, 0.7)'; } else if (percentile >= 40) { - return 'rgba(255, 215, 0, 0.5)'; - } else if (percentile >= 20) { - return 'rgba(250, 128, 114, 0.5)'; + // salmon + return 'rgba(250, 128, 114, 0.7)'; } else { - return 'rgba(220, 20, 60, 0.5)'; + return 'rgba(211, 211, 211, 0.7)'; } }); } } -export { initChart }; +export { initBar }; diff --git a/js/chart_data.js b/js/chart_data.js index a46c394..a1015e4 100644 --- a/js/chart_data.js +++ b/js/chart_data.js @@ -1,5 +1,5 @@ function calculateChartData(indivStats, events) { - let playerPosition = "Defensive Back"; + let playerPosition = "DB"; let playerStats = []; let playerStatsValues = []; let positionStatsValues = []; @@ -95,6 +95,9 @@ function calculateChartData(indivStats, events) { }); function getCalculatedData() { + + console.log("Player Stats", playerStats); + console.log("Player Percentiles", playerPercentiles); return { positionMedians, playerPercentiles, diff --git a/js/main.js b/js/main.js index f5d51fe..d4b67bc 100644 --- a/js/main.js +++ b/js/main.js @@ -1,6 +1,7 @@ -import { initChart } from './barchart.js'; +import { initBar } from './barchart.js'; import { initStatEntry } from './stat_entry.js'; import { calculateChartData } from './chart_data.js'; +import { initRadar } from './radar.js'; //import { getStationReports } from './firebase.js'; // Fetch data @@ -22,13 +23,24 @@ initStatEntry(statListEl, positionDropdownEl, statNames, positions, events); let chartData = calculateChartData(indivStats, events); // Get chart elements -const pctChartEl = document.querySelector('#percentile-chart'); +const strengthEl = document.querySelector('#strength-chart'); +const powerEl = document.querySelector('#power-chart'); +const speedEl = document.querySelector('#speed-chart'); +const agilityEl = document.querySelector('#agility-chart'); +const anthroEl = document.querySelector('#anthro-chart'); +const radarEl = document.querySelector('#radar-chart'); // Render charts function updateCharts() { const { positionMedians, playerPercentiles, playerStats, playerStatsValues } = chartData.getCalculatedData(); - initChart(pctChartEl, positionMedians, playerStats, playerStatsValues, playerPercentiles); + initBar(strengthEl, positionMedians, playerStats, playerStatsValues, playerPercentiles); + initBar(powerEl, positionMedians, playerStats, playerStatsValues, playerPercentiles); + initBar(speedEl, positionMedians, playerStats, playerStatsValues, playerPercentiles); + initBar(agilityEl, positionMedians, playerStats, playerStatsValues, playerPercentiles); + initBar(anthroEl, positionMedians, playerStats, playerStatsValues, playerPercentiles); + + initRadar(radarEl, positionMedians, statNames, playerStats, playerPercentiles); } // Listen for changes in stat or position diff --git a/js/radar.js b/js/radar.js new file mode 100644 index 0000000..e689603 --- /dev/null +++ b/js/radar.js @@ -0,0 +1,46 @@ +//const radarInstances = {}; + +function initRadar(radarEl, positionMedians, statNames, playerStats, playerPercentiles) { + + +//radarInstances[radarEl.id] +//= +bb.generate({ + title: { + text: "ATHLETE PROFILE" + }, + data: { + x: "x", + columns: [ + ["x", "Strength", "Power", "Speed", "Agility"], + ["Athlete", 80, 60, 30, 55], + ["Position Median", 50, 50, 50, 50], + ], + type: "radar", + labels: true, + colors: { + "Position Median": "lightgray", + "Athlete": "magenta" + } + }, + radar: { + axis: { + max: 100 + }, + level: { + depth: 2 + }, + direction: { + clockwise: false + } + }, + bindto: "#radar-chart" + }); + +/* if (radarInstances[radarEl.id]) { + radarInstances[radarEl.id].destroy(); + } */ + +} + +export { initRadar }; diff --git a/js/stat_entry.js b/js/stat_entry.js index ccc7819..83998da 100644 --- a/js/stat_entry.js +++ b/js/stat_entry.js @@ -89,7 +89,7 @@ function initStatEntry(statListEl, positionRadioEl, stats, positions, events) { } } - function populateDropdown(positions) { + function populateDropdown() { radioEl.innerHTML = ''; const dropdown = initDropdownItems(); @@ -98,18 +98,20 @@ function initStatEntry(statListEl, positionRadioEl, stats, positions, events) { dropdown.addEventListener('change', handleDropdownChange); } - populateDropdown(positions); + populateDropdown(); populateList(stats); + // Handle stat entry function handleNumEntry(evt) { const numInput = evt.target; const statName = numInput.name; - const filled = numInput.value !== '' && numInput.value !== null && numInput.value > 0; + //const filled = numInput.value !== '' && numInput.value !== null && numInput.value > 0; + const filled = numInput.value.trim() !== '' && parseFloat(numInput.value) > 0; const statValue = filled ? parseFloat(numInput.value) : null; numInput.setCustomValidity(''); - if (!numInput.checkValidity() || numInput.value <= 0) { + if (!numInput.checkValidity() || parseFloat(numInput.value) <= 0) { numInput.setCustomValidity('Please enter a valid number'); numInput.reportValidity(); return; @@ -121,6 +123,7 @@ function initStatEntry(statListEl, positionRadioEl, stats, positions, events) { events.dispatchEvent(event); } + // Handle position selection function handleDropdownChange(evt) { const selectedPosition = evt.target.value; From 2d5820abcc0f1453967ca1ffe99934fcfa88ca12 Mon Sep 17 00:00:00 2001 From: Anna Duan Date: Fri, 8 Nov 2024 19:37:13 -0500 Subject: [PATCH 6/9] radar chart bug - something is undefined when all 4 category percentiles are populated.. --- js/barchart.js | 5 ----- js/chart_data.js | 40 +++++++++++++++++++++++++++++++++++++--- js/main.js | 5 ++--- js/radar.js | 40 +++++++++++++++++++++++++--------------- 4 files changed, 64 insertions(+), 26 deletions(-) diff --git a/js/barchart.js b/js/barchart.js index 72ad7a1..3430def 100644 --- a/js/barchart.js +++ b/js/barchart.js @@ -12,7 +12,6 @@ function initBar(barEl, positionMedians, statNames, playerStats, playerPercentil const category = barEl.id.replace('-chart', '').toLowerCase(); const metrics = statCategories[category]; - const filteredStatNames = []; const filteredPositionMedians = []; const filteredPlayerStats = []; @@ -33,7 +32,6 @@ function initBar(barEl, positionMedians, statNames, playerStats, playerPercentil ]; if (barInstances[barEl.id]) { - console.log(`Destroying chart with id: ${barEl.id}`); barInstances[barEl.id].destroy(); } @@ -54,9 +52,6 @@ function initBar(barEl, positionMedians, statNames, playerStats, playerPercentil return color; } }, - transition: { - duration: 100 - }, bar: { padding: 1, radius: { diff --git a/js/chart_data.js b/js/chart_data.js index a1015e4..bc20c10 100644 --- a/js/chart_data.js +++ b/js/chart_data.js @@ -5,11 +5,42 @@ function calculateChartData(indivStats, events) { let positionStatsValues = []; let positionMedians = []; let playerPercentiles = []; + let categoryPercentiles = []; + // Stats that are better when lower const inversePercentileStats = [ 'Flying 10', '10-Yard Sprint', '60-Yard Shuttle', 'L Drill', 'Pro Agility' ]; + // Stat categories + const statCategories = { + speed: ['10-Yard Sprint', 'Flying 10'], + agility: ['Pro Agility', 'L Drill', '60-Yard Shuttle'], + power: ['Vertical Jump', 'Broad Jump', 'Hang Clean', 'Power Clean'], + strength: ['Squat', 'Bench', '225lb Bench'] + }; + + // Calculate mean player percentile within each stat category + function getCategoryPercentiles() { + categoryPercentiles = []; + for (const category in statCategories) { + const statNames = statCategories[category]; + const statIndexes = statNames.map(name => playerStats.indexOf(name)); + const percentiles = statIndexes + .map(index => playerPercentiles[index]) + .filter(value => value !== undefined); + + const mean = percentiles.reduce((a, b) => a + b, 0) / percentiles.length; + let categoryPercentile = Math.round(mean); + // only return if there are defined values + if (!isNaN(categoryPercentile)) { + categoryPercentiles.push(categoryPercentile); + } +} + return categoryPercentiles; + } + + // Calculate player percentiles within position group function getPercentiles() { playerPercentiles = []; @@ -32,6 +63,7 @@ function calculateChartData(indivStats, events) { return playerPercentiles; } + // Calculate position group medians (2020-2024) function getMedians() { positionMedians = []; @@ -51,6 +83,7 @@ function calculateChartData(indivStats, events) { return positionMedians; } + // Handle stat changes function updatePositionStatsValues() { positionStatsValues = playerStats.map(statName => { return indivStats[playerPosition].map(item => item[statName]); @@ -59,6 +92,7 @@ function calculateChartData(indivStats, events) { positionStatsValues = positionStatsValues.map(statArray => statArray.flat(1)); getMedians(); getPercentiles(); + getCategoryPercentiles(); } events.addEventListener('positionSelected', (evt) => { @@ -92,17 +126,17 @@ function calculateChartData(indivStats, events) { positionStatsValues = positionStatsValues.map(statArray => statArray.flat(1)); getMedians(); getPercentiles(); + getCategoryPercentiles(); }); + // Return calculated data for bar and radar chart render function getCalculatedData() { - - console.log("Player Stats", playerStats); - console.log("Player Percentiles", playerPercentiles); return { positionMedians, playerPercentiles, playerStats, playerStatsValues, + categoryPercentiles }; } diff --git a/js/main.js b/js/main.js index d4b67bc..51c52a1 100644 --- a/js/main.js +++ b/js/main.js @@ -2,7 +2,6 @@ import { initBar } from './barchart.js'; import { initStatEntry } from './stat_entry.js'; import { calculateChartData } from './chart_data.js'; import { initRadar } from './radar.js'; -//import { getStationReports } from './firebase.js'; // Fetch data const indivStatsResponse = await fetch('data/stats_2020_2024.json'); @@ -32,7 +31,7 @@ const radarEl = document.querySelector('#radar-chart'); // Render charts function updateCharts() { - const { positionMedians, playerPercentiles, playerStats, playerStatsValues } = chartData.getCalculatedData(); + const { positionMedians, playerPercentiles, playerStats, playerStatsValues, categoryPercentiles } = chartData.getCalculatedData(); initBar(strengthEl, positionMedians, playerStats, playerStatsValues, playerPercentiles); initBar(powerEl, positionMedians, playerStats, playerStatsValues, playerPercentiles); @@ -40,7 +39,7 @@ function updateCharts() { initBar(agilityEl, positionMedians, playerStats, playerStatsValues, playerPercentiles); initBar(anthroEl, positionMedians, playerStats, playerStatsValues, playerPercentiles); - initRadar(radarEl, positionMedians, statNames, playerStats, playerPercentiles); + initRadar(radarEl, categoryPercentiles); } // Listen for changes in stat or position diff --git a/js/radar.js b/js/radar.js index e689603..aa0d45b 100644 --- a/js/radar.js +++ b/js/radar.js @@ -1,25 +1,39 @@ -//const radarInstances = {}; +const radarInstances = {}; -function initRadar(radarEl, positionMedians, statNames, playerStats, playerPercentiles) { +function initRadar(radarEl, categoryPercentiles) { +console.log("Category Percentiles", categoryPercentiles); -//radarInstances[radarEl.id] -//= -bb.generate({ +let columns; + +if (categoryPercentiles.length < 4) { + columns = [ + ['x', 'SPEED', 'AGILITY', 'POWER', 'STRENGTH'], + ['Position Group', 50, 50, 50, 50] + ]; +} else { +columns = [ + ['x', 'SPEED', 'AGILITY', 'POWER', 'STRENGTH'], + ['Athlete', ...categoryPercentiles] + ['Position Group', 50, 50, 50, 50] +]; +} + +if (radarInstances[radarEl.id]) { + radarInstances[radarEl.id].destroy(); +} + +radarInstances[radarEl.id] = bb.generate({ title: { text: "ATHLETE PROFILE" }, data: { x: "x", - columns: [ - ["x", "Strength", "Power", "Speed", "Agility"], - ["Athlete", 80, 60, 30, 55], - ["Position Median", 50, 50, 50, 50], - ], + columns: columns, type: "radar", labels: true, colors: { - "Position Median": "lightgray", + "Position Group": "lightgray", "Athlete": "magenta" } }, @@ -37,10 +51,6 @@ bb.generate({ bindto: "#radar-chart" }); -/* if (radarInstances[radarEl.id]) { - radarInstances[radarEl.id].destroy(); - } */ - } export { initRadar }; From 6d7912db35c9a8f2d772479bd15eb6091e063050 Mon Sep 17 00:00:00 2001 From: Anna Duan Date: Fri, 8 Nov 2024 23:59:20 -0500 Subject: [PATCH 7/9] radar aesthetics --- js/radar.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/js/radar.js b/js/radar.js index aa0d45b..b488ab9 100644 --- a/js/radar.js +++ b/js/radar.js @@ -14,7 +14,7 @@ if (categoryPercentiles.length < 4) { } else { columns = [ ['x', 'SPEED', 'AGILITY', 'POWER', 'STRENGTH'], - ['Athlete', ...categoryPercentiles] + ['Athlete', ...categoryPercentiles], ['Position Group', 50, 50, 50, 50] ]; } @@ -33,8 +33,8 @@ radarInstances[radarEl.id] = bb.generate({ type: "radar", labels: true, colors: { - "Position Group": "lightgray", - "Athlete": "magenta" + "Position Group": "darkcyan", + "Athlete": "salmon" } }, radar: { @@ -42,11 +42,8 @@ radarInstances[radarEl.id] = bb.generate({ max: 100 }, level: { - depth: 2 + depth: 1 }, - direction: { - clockwise: false - } }, bindto: "#radar-chart" }); From 291c1a430fd628033081a73098ae7927797a44b4 Mon Sep 17 00:00:00 2001 From: Anna Duan Date: Sat, 9 Nov 2024 08:38:25 -0500 Subject: [PATCH 8/9] Dynamic radar chart --- css/styles.css | 47 +++++++++++++++++++++++------------------------ js/chart_data.js | 4 +++- js/radar.js | 8 ++++---- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/css/styles.css b/css/styles.css index 7acb3d0..f97946f 100644 --- a/css/styles.css +++ b/css/styles.css @@ -3,12 +3,6 @@ body { color: #011f5b; } -#page-name { - font-size: 2rem; - font-weight: 100; - color: #011f5b; - margin-left: 1rem; -} .header { display: flex; @@ -38,12 +32,8 @@ body { } fieldset { - height: auto; text-align: left; - text-justify: auto; - display: flex; - flex-direction: column; - background-color: rgba(0, 0, 0, 0.05); + justify-items: left; border-color: transparent; border-radius: 0.5em; } @@ -51,14 +41,14 @@ fieldset { .benchmark-menu input { border-color: transparent; font-size: 1rem; - color: gray; - opacity: 0.5; + background-color: rgb(241, 241, 241); + border-radius: 0.5em; font-weight:lighter; } select { - width: 60%; - padding: 0.5rem; + width: 40%; + padding: 0.2rem; font-size: 1rem; font-weight: 100; color: #011f5b; @@ -70,14 +60,11 @@ ul { list-style-type: none; font-weight: 100; font-size: 1rem; - text-align: left; } .input-wrapper { position: relative; width: 60%; - display: flex; - flex-direction: row; align-items: center; background-color: white; opacity: 0.75; @@ -86,7 +73,6 @@ ul { input[type="number"] { width: 100%; - padding-right: 4rem; box-sizing: border-box; appearance: textfield; } @@ -94,11 +80,8 @@ input[type="number"] { .unit { position: absolute; right: 0; - width: 4rem; - color: #cdcbcb; - pointer-events: none; - font-size: 0.9em; - z-index: 1; + width: 5rem; + color: #cdcbcb; } .benchmark-chart-area { @@ -129,4 +112,20 @@ input[type="number"] { input[type="number"]:invalid { color: red; +} + +/* Billboard Charts */ +.bb-axis.bb-axis-x path.domain { + opacity: 0; +} + +#radar-chart .bb-axis text { + fill: gray; + font-size: 0.8rem; + text-anchor: middle; +} + +.bb-title { + fill: #011f5b; + font-weight: 100; } \ No newline at end of file diff --git a/js/chart_data.js b/js/chart_data.js index bc20c10..971e88f 100644 --- a/js/chart_data.js +++ b/js/chart_data.js @@ -32,9 +32,11 @@ function calculateChartData(indivStats, events) { const mean = percentiles.reduce((a, b) => a + b, 0) / percentiles.length; let categoryPercentile = Math.round(mean); - // only return if there are defined values + // only return defined values if (!isNaN(categoryPercentile)) { categoryPercentiles.push(categoryPercentile); + } else { + categoryPercentiles.push(0); } } return categoryPercentiles; diff --git a/js/radar.js b/js/radar.js index b488ab9..199628b 100644 --- a/js/radar.js +++ b/js/radar.js @@ -14,8 +14,8 @@ if (categoryPercentiles.length < 4) { } else { columns = [ ['x', 'SPEED', 'AGILITY', 'POWER', 'STRENGTH'], - ['Athlete', ...categoryPercentiles], - ['Position Group', 50, 50, 50, 50] + ['Position Group', 50, 50, 50, 50], + ['Athlete', ...categoryPercentiles] ]; } @@ -33,8 +33,8 @@ radarInstances[radarEl.id] = bb.generate({ type: "radar", labels: true, colors: { - "Position Group": "darkcyan", - "Athlete": "salmon" + "Position Group": "black", + "Athlete": "darkcyan" } }, radar: { From e2ca8a833bb0b066d10b7dfe0fe9a4c5ff56c747 Mon Sep 17 00:00:00 2001 From: Anna Duan Date: Sun, 10 Nov 2024 10:15:53 -0500 Subject: [PATCH 9/9] radar aesthetics --- css/styles.css | 22 ++++++++++++++++++---- index.html | 2 +- js/barchart.js | 2 +- js/radar.js | 12 ++++++------ js/stat_entry.js | 3 +++ 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/css/styles.css b/css/styles.css index f97946f..b329487 100644 --- a/css/styles.css +++ b/css/styles.css @@ -3,7 +3,6 @@ body { color: #011f5b; } - .header { display: flex; flex-direction: row; @@ -24,7 +23,7 @@ body { grid-template-columns: repeat(6, 1fr); grid-template-rows: repeat(4, 1fr); align-content: flex-start; - gap: 1rem; + gap: 0rem; } .benchmark-menu { @@ -32,10 +31,9 @@ body { } fieldset { - text-align: left; - justify-items: left; border-color: transparent; border-radius: 0.5em; + width: 70%; } .benchmark-menu input { @@ -58,6 +56,7 @@ select { ul { list-style-type: none; + padding-left: 0; font-weight: 100; font-size: 1rem; } @@ -122,10 +121,25 @@ input[type="number"]:invalid { #radar-chart .bb-axis text { fill: gray; font-size: 0.8rem; + font-weight: 100; text-anchor: middle; } .bb-title { fill: #011f5b; font-weight: 100; +} + +/* Radar Chart */ +.bb-level > polygon:nth-child(1) { + opacity: 0.3; +} + +.bb-axis-0 > line:nth-child(1) { + opacity: 0.5; +} + + +.bb-level > text:nth-child(2) { + opacity: 0; } \ No newline at end of file diff --git a/index.html b/index.html index f1ff3b6..66ef7dd 100644 --- a/index.html +++ b/index.html @@ -21,7 +21,7 @@

Select a position group to start

Select athlete position

-

Enter athlete stats to benchmark against position group

+

Enter athlete stats to benchmark against position