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 @@
+
-
+
+