diff --git a/config/analysis-modules.json b/config/analysis-modules.json index 9a20c941..5bc2eaca 100644 --- a/config/analysis-modules.json +++ b/config/analysis-modules.json @@ -85,6 +85,7 @@ "module": "Pie", "position":"main", "name":"Avg Cone Pickups", + "wholeMatch": false, "options":{ "slices":[ { @@ -115,6 +116,7 @@ "module": "Pie", "position":"main", "name":"Avg Cube Pickups", + "wholeMatch": false, "options":{ "slices":[ { @@ -521,6 +523,17 @@ "decimals": 2 } }, + { + "view": "match", + "module": "SingleDisplay", + "name": "Percent Chance of Winning", + "position": "main", + "wholeMatch": true, + "options": { + "aggrMethod" : "percentChanceOfWinning", + "decimals": 2 + } + }, { "view": "match", "module": "ColumnDisplay", @@ -549,6 +562,7 @@ "module": "Pie", "position":"main", "name":"Avg Cone Pickups", + "wholeMatch": false, "options":{ "slices":[ { diff --git a/config/analysis-pipeline.json b/config/analysis-pipeline.json index bdbbb9b9..3357b97e 100644 --- a/config/analysis-pipeline.json +++ b/config/analysis-pipeline.json @@ -491,5 +491,15 @@ "options": { "path": "scores" } - } + }, + + { + "type": "team", + "name": "standardDeviation", + "outputPath": "standardDeviation", + "options": { + "path" : "scores.total" + } + } + ] \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index cdd35d35..9255703a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "mongoose": "^6.0.6", "nodemon": "^2.0.15", "qrcode": "^1.4.4", + "simple-statistics": "^7.8.2", "socket.io": "^4.4.1" }, "engines": { @@ -2420,6 +2421,14 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" }, + "node_modules/simple-statistics": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.2.tgz", + "integrity": "sha512-evB7wiL4DSz6T9TdCTfRRqNcmnp0bllcUfZ4NBwtkdGMbXrYsZbrGxfunLPqdqCyT2Yz1NvLrKS7LVOoFBCxbA==", + "engines": { + "node": "*" + } + }, "node_modules/sliced": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", @@ -4686,6 +4695,11 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.6.tgz", "integrity": "sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==" }, + "simple-statistics": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.2.tgz", + "integrity": "sha512-evB7wiL4DSz6T9TdCTfRRqNcmnp0bllcUfZ4NBwtkdGMbXrYsZbrGxfunLPqdqCyT2Yz1NvLrKS7LVOoFBCxbA==" + }, "sliced": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", diff --git a/package.json b/package.json index 8372b399..ec98bd69 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "spot", "version": "1.0.0", - "description": "", + "description": "SPOT is an open-source modular scouting app framework for FRC developed by Team 3061 Huskie Robotics. SPOT provides a simple platform upon which a team can build a scouting app with little to no prior experience.", "main": "./src/app.js", "scripts": { "start": "node src/app.js", @@ -33,6 +33,7 @@ "mongoose": "^6.0.6", "nodemon": "^2.0.15", "qrcode": "^1.4.4", + "simple-statistics": "^7.8.2", "socket.io": "^4.4.1" } } diff --git a/src/analysis/modules/Grid/index.js b/src/analysis/modules/Grid/index.js index 89a1f912..8f71d044 100644 --- a/src/analysis/modules/Grid/index.js +++ b/src/analysis/modules/Grid/index.js @@ -16,7 +16,7 @@ class Grid { let newObj = { x:cell.x, y:cell.y, - data:teams.map((team)=>{let data = getPath(dataset.teams[team],cell.path,0).toFixed(this.moduleConfig.options.decimals);console.log(data);return data }).reduce((acc, i) => acc + parseFloat(i), 0).toFixed(this.moduleConfig.options.decimals), + data:teams.map((team)=>{let data = getPath(dataset.teams[team],cell.path,0).toFixed(this.moduleConfig.options.decimals);return data }).reduce((acc, i) => acc + parseFloat(i), 0).toFixed(this.moduleConfig.options.decimals), hex:cell.hex, } return newObj diff --git a/src/analysis/modules/Pie/index.js b/src/analysis/modules/Pie/index.js index 2fc433c9..76361e7f 100644 --- a/src/analysis/modules/Pie/index.js +++ b/src/analysis/modules/Pie/index.js @@ -8,8 +8,10 @@ class Pie { } formatData(teams, dataset) { + console.log(`pie teams recieved: ${teams}`); + let filteredTeams = teams.filter(team=>team!="|"); const values = this.moduleConfig.options.slices.map((slice) => { - const summed = teams.map(team => {console.log(team);return getPath(dataset.teams[team], slice.path)}).flat().reduce((acc, i) => acc + i, 0) + const summed = filteredTeams.map(team => {let data = getPath(dataset.teams[team], slice.path); console.log(`${slice.path}: ${data}`);return data}).flat().reduce((acc, i) => acc + i, 0) if (slice.aggrMethod == "sum") { //optionally summed return summed } else { //default is average @@ -32,7 +34,6 @@ class Pie { } ] - console.log(data) return data } diff --git a/src/analysis/modules/SingleDisplay/index.js b/src/analysis/modules/SingleDisplay/index.js index 886881ee..b817578c 100644 --- a/src/analysis/modules/SingleDisplay/index.js +++ b/src/analysis/modules/SingleDisplay/index.js @@ -14,24 +14,44 @@ class SingleDisplay { } formatData(teams, dataset) { - let summed - if (teams.length > 1) { - summed = teams.map(team => getPath(dataset.teams[team], this.moduleConfig.options.path, 0)).flat().reduce((acc, i) => acc + i, 0) + // teams = [b1,b2,b3,|,r1,r3,r3,|] + let teamsArray = teams + let summed + let formattedDisplay + if(this.moduleConfig.wholeMatch) { + let indexOfPipe = teamsArray.indexOf("|") + let alliance1 = teamsArray.slice(0, indexOfPipe) + // alliance 1 = [b1,b2,b3] + let alliance2 = teamsArray.slice(indexOfPipe+1, teamsArray.length) + alliance2 = alliance2.filter(team => team != "|") + // alliance 2 = [r1,r2,r3] + if (this.moduleConfig.options.aggrMethod == "percentChanceOfWinning") { //optionally percent chance of winning + formattedDisplay = this.compareAlliances(alliance1, alliance2, dataset) + formattedDisplay = (formattedDisplay * 100).toFixed(2).toString()+"%"; + } else { //default is undefined + formattedDisplay = "0%" + } + } else { - summed = getPath(dataset.teams[teams[0]], this.moduleConfig.options.path, 0) - } + if (teams.length > 1) { + summed = teams.map(team => getPath(dataset.teams[team], this.moduleConfig.options.path, 0)).flat().reduce((acc, i) => acc + i, 0) + } else { + summed = getPath(dataset.teams[teams[0]], this.moduleConfig.options.path, 0) + } - let formattedDisplay - if (this.moduleConfig.options.aggrMethod == "sum") { //optionally summed - formattedDisplay = summed - } else { //default is average - formattedDisplay = summed / teams.length + if (this.moduleConfig.options.aggrMethod == "sum") { //optionally summed + formattedDisplay = summed + } else { //default is average + formattedDisplay = summed / teams.length + } } - formattedDisplay = this.applyModifiers(formattedDisplay) if (isNaN(formattedDisplay) || formattedDisplay == this.moduleConfig.options.hideIfValue) { - formattedDisplay = "—" + if(!this.moduleConfig.wholeMatch){ + formattedDisplay = "—" + } + } else { if (this.moduleConfig.options.decimals !== undefined) { formattedDisplay = formattedDisplay.toFixed(this.moduleConfig.options.decimals) @@ -41,7 +61,6 @@ class SingleDisplay { formattedDisplay += this.moduleConfig.options.unit } } - return formattedDisplay } @@ -61,4 +80,61 @@ class SingleDisplay { this.header.innerText = this.moduleConfig.name this.display.innerText = data } + + /** + * + * @param {*an allaicne of any length*} alliance1 + * @param {*an alliance to compare to of any length*} alliance2 + * @param {*the data set that holds the infomation of the teams*} dataset + * @returns {*the avg difference in score between allaicne 1 and allaicne 2*} + */ + matchAverage(alliance1, alliance2, dataset){ + let alliance1Avg = 0 + for (const a of alliance1) { + alliance1Avg += getPath(dataset.teams[a],"averageScores.total",0) + } + let alliance2Avg = 0 + for (const a of alliance2) { + alliance2Avg += getPath(dataset.teams[a],"averageScores.total",0) + } + return alliance1Avg - alliance2Avg + } + + /** + * + * @param {*an allaicne of any length*} alliance1 + * @param {*an alliance to compare to of any length*} alliance2 + * @param {*the data set that holds the infomation of the teams*} dataset + * @returns {*the standard deveation of the given match*} + */ + matchStandardDeviation(alliance1, alliance2, dataset) { + let alliance1SD = 0 + for (const a of alliance1) { + let data = getPath(dataset.teams[a],"standardDeviation",0) + alliance1SD += Math.pow(data, 2) + } + alliance1SD = Math.sqrt(alliance1SD) + let alliance2SD = 0 + for (const a of alliance2) { + let data = getPath(dataset.teams[a],"standardDeviation",0) + alliance2SD += Math.pow(data, 2) + } + alliance2SD = Math.sqrt(alliance2SD) + return Math.sqrt(Math.pow(alliance1SD, 2) + Math.pow(alliance2SD, 2)) + } + + /** + * + * @param {*an allaicne of any length*} alliance1 + * @param {*an alliance to compare to of any length*} alliance2 + * @param {*the data set that holds the infomation of the teams*} dataset + * @returns {*the percent chance that alliance1 will win this match*} + */ + compareAlliances(alliance1, alliance2, dataset) { + let zscore = ss.zScore(0, this.matchAverage(alliance1, alliance2, dataset), this.matchStandardDeviation(alliance1, alliance2, dataset)) + let probAlliance2Wins = ss.cumulativeStdNormalProbability(zscore) + return 1 - probAlliance2Wins; + } + + } \ No newline at end of file diff --git a/src/analysis/public/js/elements.js b/src/analysis/public/js/elements.js index ffa6570d..857b1602 100644 --- a/src/analysis/public/js/elements.js +++ b/src/analysis/public/js/elements.js @@ -12,4 +12,5 @@ const sideList = document.getElementById("side-list") const searchInput = document.getElementById("search-input") const matchViewSwitch = document.getElementById("match-view-switch") const leftAllianceModules = document.getElementById("left-alliance-modules") -const rightAllianceModules = document.getElementById("right-alliance-modules") \ No newline at end of file +const rightAllianceModules = document.getElementById("right-alliance-modules") +// const bothAllianceModules = document.getElementById("both-alliance-modules") \ No newline at end of file diff --git a/src/analysis/public/js/script.js b/src/analysis/public/js/script.js index 9d9d363c..cc48d6e0 100644 --- a/src/analysis/public/js/script.js +++ b/src/analysis/public/js/script.js @@ -17,7 +17,8 @@ if ('serviceWorker' in navigator) { team: [], match: { left: [], - right: [] + right: [], + both: [] } } @@ -222,7 +223,7 @@ if ('serviceWorker' in navigator) { } }) - //create match module objects and append placeholders to module list elements + //create match module objects and append placeholders to module list elements for (const module of modulesConfig.filter(m => m.view == "match")) { const leftModuleObject = new moduleClasses[module.module](module) leftAllianceModules.appendChild(leftModuleObject.container) @@ -237,14 +238,35 @@ if ('serviceWorker' in navigator) { //call setData on every module in matches async function setMatchModules(alliances) { for (const module of modules.match.left) { - const displayedAlliances = alliances[0].filter(teamNumber => { - if (!module.moduleConfig.separate && Object.keys(dataset.teams[teamNumber]).filter(prop => prop !== "manual").length == 0) { + console.log(module.moduleConfig.name) + var displayedAlliances = alliances[0].filter(teamNumber => { + if(teamNumber == "|"){return false} + if (!module.moduleConfig.separate && Object.keys(dataset.teams[teamNumber]).filter(prop => prop !== "manual").length == 0) { return false } - + return true }) - + if(module.moduleConfig.wholeMatch) { + let allTeams = alliances[0] + console.log(`alliances script.js ${alliances}`) + allTeams.push('|') + allTeams = allTeams.concat(alliances[1]) + console.log(`all teams: ${allTeams}`) + displayedAlliances = allTeams.filter(teamNumber => { + if (!module.moduleConfig.separate && teamNumber != "|" && Object.keys(dataset.teams[teamNumber]).filter(prop => prop !== "manual").length == 0) { + return false + } + return true + }) + console.log(`displayed alliances: ${displayedAlliances}`) + if (displayedAlliances.length !== 0) { + module.container.classList.remove("hidden") + await module.setData(await module.formatData(allTeams, dataset)) + } else { + module.container.classList.add("hidden") + } + } if (displayedAlliances.length !== 0) { module.container.classList.remove("hidden") await module.setData(await module.formatData(displayedAlliances, dataset)) @@ -254,14 +276,35 @@ if ('serviceWorker' in navigator) { } for (const module of modules.match.right) { - const displayedAlliances = alliances[1].filter(teamNumber => { + console.log(module.moduleConfig.name) + var displayedAlliances = alliances[1].filter(teamNumber => { + if(teamNumber == "|"){return false} if (!module.moduleConfig.separate && Object.keys(dataset.teams[teamNumber]).filter(prop => prop !== "manual").length == 0) { return false } - + return true }) - + if(module.moduleConfig.wholeMatch) { + let allTeams = alliances[1] + allTeams.push('|') + allTeams = allTeams.concat(alliances[0]) + console.log(`all teams: ${allTeams}`) + var displayedAlliances = allTeams.filter(teamNumber => { + if (!module.moduleConfig.separate && teamNumber != "|" && Object.keys(dataset.teams[teamNumber]).filter(prop => prop !== "manual").length == 0) { + return false + } + + return true + }) + console.log(`displayed alliances: ${displayedAlliances}`) + if (displayedAlliances.length !== 0) { + module.container.classList.remove("hidden") + await module.setData(await module.formatData(displayedAlliances, dataset)) + } else { + module.container.classList.add("hidden") + } + } if (displayedAlliances.length !== 0) { module.container.classList.remove("hidden") await module.setData(await module.formatData(displayedAlliances, dataset)) diff --git a/src/analysis/public/js/util.js b/src/analysis/public/js/util.js index fa696819..35795abc 100644 --- a/src/analysis/public/js/util.js +++ b/src/analysis/public/js/util.js @@ -22,7 +22,7 @@ function clearDiv(div) { function getPath(obj,path,ifnone=ThrowError) { if (typeof obj === "undefined") { if (ifnone == ThrowError) { - throw new Error("path not traversable!"); + throw new Error(`path ${path} not traversable!`); } else { return ifnone; } diff --git a/src/analysis/transformers/standardDeviation.js b/src/analysis/transformers/standardDeviation.js new file mode 100644 index 00000000..45750f93 --- /dev/null +++ b/src/analysis/transformers/standardDeviation.js @@ -0,0 +1,33 @@ +const{ getPath, setPath } = require("../../lib/util"); +const{ DataTransformer } = require("../DataTransformer"); + +module.exports = { + /** + find standard deviation + @type { DataTransformer } + @param options.path {String} + */ + /* + const n = array.length + const mean = array.reduce((a, b) => a + b) / n + return Math.sqrt(array.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n) + */ + team: new DataTransformer("standardDeviation", (dataset, outputPath, options) => { + for (let [teamNumber,team] of Object.entries(dataset.teams)) { + let teamTmps = dataset.tmps.filter(x=>x.robotNumber == teamNumber); //only the tmps that are this team's + let scores = [] + for (let tmp of teamTmps) { + scores.push(getPath(tmp,options.path)) + } + let n = scores.length + let mean = scores.reduce((a, b) => a + b) / n + let sd = Math.sqrt(scores.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n) + console.log(scores) + console.log(mean) + console.log(sd) + setPath(team,outputPath,sd) + } + + return dataset; + }) +} \ No newline at end of file diff --git a/src/analysis/views/index.ejs b/src/analysis/views/index.ejs index 1e6d4ffa..a33b0865 100644 --- a/src/analysis/views/index.ejs +++ b/src/analysis/views/index.ejs @@ -124,13 +124,15 @@ + - + + \ No newline at end of file