diff --git a/www/points.json b/www/points.json new file mode 100644 index 0000000..f840e8c --- /dev/null +++ b/www/points.json @@ -0,0 +1,339 @@ +{ + "points": [ +[1429583964, "1", "nocode", 1], +[1429583994, "1", "nocode", 2], +[1429583998, "2", "nocode", 1], +[1429584021, "1", "nocode", 3], +[1429584035, "1", "nocode", 4], +[1429584085, "2", "js", 1], +[1429584118, "3", "codebreaking", 1], +[1429584201, "1", "nocode", 10], +[1429584403, "1", "js", 1], +[1429584466, "3", "net-re", 1], +[1429584776, "4", "nocode", 1], +[1429584851, "4", "nocode", 2], +[1429585153, "4", "nocode", 3], +[1429585279, "4", "nocode", 4], +[1429585297, "5", "nocode", 1], +[1429585326, "5", "nocode", 2], +[1429585381, "6", "nocode", 1], +[1429585386, "5", "nocode", 3], +[1429585440, "6", "nocode", 2], +[1429585521, "4", "nocode", 10], +[1429585664, "6", "nocode", 3], +[1429585689, "6", "nocode", 4], +[1429585744, "6", "nocode", 10], +[1429585819, "1", "net-re", 1], +[1429586010, "3", "js", 1], +[1429586189, "6", "js", 1], +[1429586546, "7", "nocode", 1], +[1429586574, "7", "nocode", 2], +[1429586648, "7", "nocode", 3], +[1429586656, "3", "js", 2], +[1429586661, "8", "nocode", 2], +[1429586700, "7", "nocode", 4], +[1429586756, "7", "nocode", 10], +[1429586770, "8", "nocode", 1], +[1429586844, "8", "js", 1], +[1429587055, "6", "net-re", 1], +[1429587072, "3", "nocode", 1], +[1429587088, "3", "nocode", 2], +[1429587102, "3", "nocode", 3], +[1429587117, "3", "nocode", 4], +[1429587137, "3", "nocode", 10], +[1429587268, "1", "codebreaking", 1], +[1429587514, "3", "nocode", 20], +[1429587821, "9", "nocode", 1], +[1429587843, "9", "nocode", 2], +[1429587892, "8", "codebreaking", 1], +[1429587983, "9", "nocode", 3], +[1429588070, "9", "nocode", 4], +[1429588116, "1", "codebreaking", 2], +[1429588141, "3", "net-re", 2], +[1429588196, "4", "nocode", 20], +[1429588236, "10", "nocode", 1], +[1429588260, "10", "nocode", 2], +[1429588376, "7", "nocode", 20], +[1429588386, "10", "nocode", 3], +[1429588519, "7", "js", 1], +[1429588613, "10", "nocode", 4], +[1429588960, "9", "nocode", 10], +[1429589154, "11", "nocode", 1], +[1429589189, "11", "nocode", 2], +[1429589215, "11", "nocode", 3], +[1429589325, "11", "nocode", 10], +[1429589534, "11", "nocode", 4], +[1429589605, "4", "bonus", 30], +[1429589805, "3", "net-re", 3], +[1429590136, "10", "net-re", 1], +[1429589817, "10", "js", 1], +[1429589867, "10", "codebreaking", 1], +[1429590137, "9", "js", 1], +[1429590009, "1", "net-re", 2], +[1429590285, "10", "net-re", 2], +[1429590406, "11", "js", 1], +[1429590574, "4", "nocode", 30], +[1429590610, "3", "codebreaking", 2], +[1429590642, "9", "codebreaking", 1], +[1429590666, "11", "nocode", 30], +[1429590775, "3", "codebreaking", 4], +[1429590900, "1", "net-re", 3], +[1429590909, "11", "net-re", 1], +[1429591066, "1", "bonus", 10], +[1429591073, "11", "net-re", 2], +[1429591159, "11", "net-re", 3], +[1429591392, "1", "net-re", 4], +[1429591600, "11", "net-re", 4], +[1429591615, "11", "js", 2], +[1429591620, "11", "codebreaking", 1], +[1429591953, "3", "net-re", 4], +[1429592104, "1", "net-re", 5], +[1429592528, "12", "nocode", 1], +[1429592555, "12", "nocode", 3], +[1429592595, "12", "nocode", 2], +[1429592634, "12", "nocode", 4], +[1429592777, "1", "net-re", 6], +[1429592818, "11", "codebreaking", 2], +[1429592954, "12", "nocode", 10], +[1429593070, "11", "codebreaking", 4], +[1429593288, "11", "nocode", 20], +[1429593393, "9", "nocode", 20], +[1429593815, "11", "bonus", 10], +[1429593885, "3", "net-re", 5], +[1429594213, "12", "js", 1], +[1429594226, "3", "net-re", 6], +[1429594312, "12", "codebreaking", 1], +[1429594645, "1", "bonus", 10], +[1429594642, "12", "codebreaking", 2], +[1429595082, "1", "net-re", 7], +[1429595683, "3", "net-re", 7], +[1429596041, "9", "nocode", 30], +[1429596823, "3", "nocode", 50], +[1429597683, "1", "net-re", 8], +[1429594279, "10", "codebreaking", 2], +[1429594357, "10", "codebreaking", 4], +[1429594399, "10", "codebreaking", 5], +[1429594404, "9", "net-re", 2], +[1429594431, "9", "net-re", 1], +[1429594458, "13", "nocode", 1], +[1429594529, "13", "js", 1], +[1429595408, "10", "net-re", 4], +[1429595408, "13", "net-re", 1], +[1429595431, "13", "codebreaking", 1], +[1429595560, "13", "nocode", 2], +[1429595692, "10", "codebreaking", 6], +[1429596133, "14", "js", 1], +[1429597686, "13", "codebreaking", 2], +[1429597777, "13", "codebreaking", 4], +[1429598384, "9", "net-re", 3], +[1429598816, "14", "js", 2], +[1429598849, "13", "js", 2], +[1429599165, "15", "nocode", 1], +[1429599169, "11", "net-re", 5], +[1429599246, "15", "nocode", 2], +[1429599491, "11", "net-re", 6], +[1429599556, "14", "codebreaking", 1], +[1429599570, "15", "nocode", 4], +[1429599617, "15", "nocode", 3], +[1429599687, "11", "net-re", 7], +[1429599708, "15", "nocode", 10], +[1429600044, "11", "net-re", 10], +[1429600115, "11", "bonus", 10], +[1429600186, "14", "codebreaking", 2], +[1429600409, "11", "net-re", 20], +[1429600635, "11", "net-re", 30], +[1429600960, "15", "js", 1], +[1429593857, "14", "codebreaking", 4], +[1429594041, "16", "nocode", 1], +[1429594056, "16", "nocode", 2], +[1429594100, "16", "nocode", 3], +[1429594113, "16", "nocode", 4], +[1429594126, "14", "net-re", 1], +[1429594238, "14", "net-re", 2], +[1429594326, "9", "codebreaking", 2], +[1429594335, "1", "net-re", 10], +[1429594387, "16", "nocode", 10], +[1429594721, "14", "nocode", 1], +[1429594776, "14", "nocode", 2], +[1429594989, "14", "nocode", 3], +[1429595014, "14", "nocode", 4], +[1429595105, "14", "nocode", 10], +[1429595464, "15", "js", 2], +[1429596277, "6", "codebreaking", 1], +[1429596292, "14", "nocode", 20], +[1429596364, "17", "nocode", 1], +[1429596400, "17", "js", 1], +[1429596776, "15", "net-re", 1], +[1429597027, "15", "net-re", 2], +[1429597044, "1", "net-re", 20], +[1429597067, "16", "net-re", 20], +[1429597214, "16", "js", 1], +[1429597234, "16", "codebreaking", 1], +[1429597287, "17", "nocode", 2], +[1429597423, "16", "codebreaking", 2], +[1429597488, "17", "nocode", 3], +[1429597627, "17", "nocode", 4], +[1429597659, "16", "codebreaking", 4], +[1429597676, "1", "codebreaking", 4], +[1429597757, "1", "codebreaking", 5], +[1429597772, "16", "codebreaking", 5], +[1429597844, "17", "nocode", 10], +[1429597882, "1", "codebreaking", 6], +[1429597895, "16", "codebreaking", 6], +[1429598048, "1", "codebreaking", 7], +[1429598063, "16", "codebreaking", 7], +[1429598111, "1", "codebreaking", 8], +[1429598123, "16", "codebreaking", 8], +[1429598176, "16", "codebreaking", 20], +[1429598188, "1", "codebreaking", 20], +[1429598227, "14", "net-re", 4], +[1429599449, "1", "nocode", 20], +[1429599466, "16", "nocode", 20], +[1429600544, "17", "nocode", 20], +[1429600741, "14", "net-re", 5], +[1429600892, "14", "net-re", 6], +[1429601090, "11", "net-re", 100], +[1429601316, "11", "net-re", 200], +[1429601600, "11", "net-re", 250], +[1429601700, "14", "net-re", 7], +[1429601907, "11", "net-re", 300], +[1429602270, "11", "js", 3], +[1429602581, "14", "net-re", 10], +[1429603096, "18", "codebreaking", 1], +[1429603148, "18", "nocode", 1], +[1429603173, "18", "nocode", 2], +[1429603273, "18", "js", 1], +[1429603277, "18", "nocode", 3], +[1429603325, "18", "nocode", 4], +[1429603478, "9", "codebreaking", 4], +[1429603509, "18", "nocode", 10], +[1429603853, "9", "codebreaking", 5], +[1429603979, "18", "codebreaking", 2], +[1429604211, "18", "codebreaking", 4], +[1429604515, "9", "codebreaking", 6], +[1429604535, "18", "codebreaking", 5], +[1429605613, "15", "net-re", 4], +[1429605764, "13", "nocode", 4], +[1429605779, "3", "nocode", 30], +[1429606493, "13", "nocode", 10], +[1429606596, "3", "nocode", 80], +[1429606712, "11", "nocode", 50], +[1429606728, "3", "nocode", 90], +[1429606762, "11", "nocode", 80], +[1429606877, "16", "nocode", 50], +[1429606962, "16", "nocode", 30], +[1429607144, "4", "nocode", 50], +[1429607428, "19", "nocode", 1], +[1429607453, "19", "nocode", 2], +[1429607877, "11", "nocode", 90], +[1429607943, "19", "nocode", 3], +[1429607997, "19", "nocode", 4], +[1429608039, "16", "nocode", 80], +[1429608054, "19", "js", 1], +[1429608083, "19", "nocode", 10], +[1429608226, "16", "nocode", 90], +[1429608430, "13", "nocode", 3], +[1429608475, "3", "codebreaking", 7], +[1429608514, "20", "nocode", 1], +[1429608537, "20", "nocode", 2], +[1429608614, "20", "nocode", 4], +[1429608689, "19", "nocode", 20], +[1429608707, "20", "js", 1], +[1429608843, "3", "net-re", 250], +[1429609088, "20", "net-re", 1], +[1429609128, "20", "codebreaking", 1], +[1429609272, "19", "codebreaking", 1], +[1429609341, "20", "nocode", 10], +[1429609586, "19", "net-re", 1], +[1429609876, "19", "nocode", 30], +[1429609981, "16", "net-re", 2], +[1429610335, "16", "net-re", 1], +[1429610755, "20", "js", 2], +[1429610777, "19", "js", 2], +[1429610922, "1", "js", 2], +[1429611472, "16", "js", 2], +[1429611695, "3", "net-re", 400], +[1429612382, "13", "net-re", 250], +[1429612600, "20", "codebreaking", 7], +[1429612771, "16", "nocode", 100], +[1429612773, "1", "nocode", 100], +[1429612818, "16", "bonus", 10], +[1429612825, "1", "bonus", 10], +[1429613065, "1", "net-re", 200], +[1429613837, "10", "net-re", 200], +[1429613970, "1", "net-re", 250], +[1429614300, "13", "net-re", 10], +[1429614363, "16", "net-re", 3], +[1429614610, "10", "codebreaking", 20], +[1429614873, "13", "net-re", 20], +[1429614939, "16", "net-re", 4], +[1429615440, "16", "net-re", 5], +[1429594321, "10", "net-re", 100], +[1429593963, "10", "net-re", 250], +[1429598332, "16", "net-re", 6], +[1429598544, "21", "nocode", 1], +[1429598631, "21", "nocode", 4], +[1429598680, "21", "nocode", 10], +[1429598834, "21", "nocode", 20], +[1429598884, "21", "nocode", 30], +[1429598978, "21", "codebreaking", 1], +[1429598994, "11", "codebreaking", 6], +[1429599012, "11", "codebreaking", 7], +[1429599029, "11", "codebreaking", 8], +[1429599154, "21", "codebreaking", 2], +[1429599164, "11", "net-re", 400], +[1429599813, "21", "codebreaking", 4], +[1429600483, "11", "js", 10], +[1429600712, "21", "codebreaking", 5], +[1429601019, "21", "js", 1], +[1429601089, "21", "js", 2], +[1429601324, "21", "nocode", 2], +[1429601491, "21", "nocode", 3], +[1429601511, "22", "codebreaking", 1], +[1429602117, "11", "nocode", 100], +[1429602389, "1", "net-re", 100], +[1429602462, "23", "nocode", 200], +[1429602515, "23", "nocode", 1], +[1429602525, "23", "nocode", 2], +[1429602540, "23", "nocode", 3], +[1429602550, "23", "nocode", 4], +[1429602582, "23", "nocode", 10], +[1429602685, "23", "codebreaking", 1], +[1429602737, "21", "nocode", 200], +[1429602747, "23", "codebreaking", 2], +[1429602767, "23", "codebreaking", 4], +[1429602824, "11", "net-re", 700], +[1429602900, "16", "net-re", 100], +[1429602990, "16", "net-re", 200], +[1429603039, "22", "nocode", 200], +[1429603308, "23", "codebreaking", 5], +[1429603374, "23", "js", 1], +[1429603607, "16", "net-re", 250], +[1429603931, "11", "net-re", 800] + ], + "teams": { +"1": "INNSOC", +"2": "Lost", +"3": "TeamSoloTF", +"4": "BEnergy", +"5": "Dreamweaver", +"6": "Ghirahim", +"7": "Awesome ", +"8": "sn3", +"9": "CybrK0pS", +"10": "dio1911", +"11": "InvaderZed", +"12": "Energy.gov", +"13": "PPPL", +"14": "OR_Ninjas", +"15": "Placebo", +"16": "WiFI-Ninja", +"17": "OST-1", +"18": "WestCoast3", +"19": "Dathcha", +"20": "Last Mohigan", +"21": "cloud", +"22": "Stephen", +"23": "Prima" + } +} diff --git a/www/scoreboard-llnl-all.html b/www/scoreboard-llnl-all.html new file mode 100644 index 0000000..4050e3f --- /dev/null +++ b/www/scoreboard-llnl-all.html @@ -0,0 +1,50 @@ + + + + Scoreboard + + + + + + + +

Scoreboard

+
+ + + diff --git a/www/scoreboard-llnl-timeline.html b/www/scoreboard-llnl-timeline.html new file mode 100644 index 0000000..3943290 --- /dev/null +++ b/www/scoreboard-llnl-timeline.html @@ -0,0 +1,50 @@ + + + + Scoreboard + + + + + + + +

Scoreboard

+
+ + + diff --git a/www/scoreboard-llnl.js b/www/scoreboard-llnl.js new file mode 100644 index 0000000..62b14e8 --- /dev/null +++ b/www/scoreboard-llnl.js @@ -0,0 +1,531 @@ +function loadJSON(url, callback) { + function loaded(e) { + callback(e.target.response); + } + var xhr = new XMLHttpRequest() + xhr.onload = loaded; + xhr.open("GET", url, true); + xhr.responseType = "json"; + xhr.send(); +} + +function toObject(arr) { + var rv = {}; + for (var i = 0; i < arr.length; ++i) + if (arr[i] !== undefined) rv[i] = arr[i]; + return rv; +} + +var updateInterval; + +function scoreboard(element, continuous, mode, interval) { + if(updateInterval) + { + clearInterval(updateInterval); + } + function update(state) { + console.log("Updating"); + var teamnames = state["teams"]; + var pointslog = state["points"]; + var highscore = {}; + var teams = {}; + + function pointsCompare(a, b) { + return a[0] - b[0]; + } + pointslog.sort(pointsCompare); + var minTime = pointslog[0][0]; + var maxTime = pointslog[pointslog.length - 1][0]; + + var allQuestions = {}; + + for (var i in pointslog) + { + var entry = pointslog[i]; + var timestamp = entry[0]; + var teamhash = entry[1]; + var category = entry[2]; + var points = entry[3]; + + var catPoints = {}; + if(category in allQuestions) + { + catPoints = allQuestions[category]; + } + else + { + catPoints["total"] = 0; + } + + if(!(points in catPoints)) + { + catPoints[points] = 1; + catPoints["total"] = catPoints["total"] + points; + } + else + { + catPoints[points] = catPoints[points] + 1; + } + + allQuestions[category] = catPoints; + } + + // Dole out points + for (var i in pointslog) { + var entry = pointslog[i]; + var timestamp = entry[0]; + var teamhash = entry[1]; + var category = entry[2]; + var points = entry[3]; + + var team = teams[teamhash] || {__hash__: teamhash}; + + // Add points to team's points for that category + team[category] = (team[category] || 0) + points; + + // Record highest score in a category + highscore[category] = Math.max(highscore[category] || 0, team[category]); + + teams[teamhash] = team; + } + + // Sort by team score + function teamScore(t) { + var score = 0; + + for (var category in highscore) { + score += (t[category] || 0) / highscore[category]; + } + // XXX: This function really shouldn't have side effects. + t.__score__ = score; + return score; + } + function pointScore(points, category) + { + return points / highscore[category] + } + function teamCompare(a, b) { + return teamScore(a) - teamScore(b); + } + + var winners = []; + for (var i in teams) { + winners.push(teams[i]); + } + if (winners.length == 0) { + // No teams! + return; + } + winners.sort(teamCompare); + winners.reverse(); + + // Clear out the element we're about to populate + while (element.lastChild) { + element.removeChild(element.lastChild); + } + + // Populate! + var topActualScore = winners[0].__score__; + + + if(mode == "time") + { + var colorScale = d3.schemeCategory10; + + var teamLines = {}; + var reverseTeam = {}; + for(var i in pointslog) + { + var entry = pointslog[i]; + var timestamp = entry[0]; + var teamhash = entry[1]; + var category = entry[2]; + var points = entry[3]; + var teamname = teamnames[teamhash]; + reverseTeam[teamname] = teamhash; + points = pointScore(points, category); + + if(!(teamname in teamLines)) + { + var teamHistory = [[timestamp, points, category, entry[3], [minTime, 0, category, 0]]]; + teamLines[teamname] = teamHistory; + } + else + { + var teamHistory = teamLines[teamname]; + teamHistory.push([timestamp, points + teamHistory[teamHistory.length - 1][1], category, entry[3], teamHistory[teamHistory.length - 1]]); + } + } + + //console.log(teamLines); + + var graph = document.createElement("svg"); + graph.id = "graph"; + graph.style.width="90%"; + graph.style.height="40em"; + graph.style.backgroundColor = "white"; + graph.style.display = "table"; + var holdingDiv = document.createElement("div"); + holdingDiv.align="center"; + holdingDiv.id="holding"; + element.appendChild(holdingDiv); + holdingDiv.appendChild(graph); + + var margins = 40; + + var width = graph.offsetWidth; + var height = graph.offsetHeight; + + //var xScale = d3.scaleLinear().range([minTime, maxTime]); + //var yScale = d3.scaleLinear().range([0, topActualScore]); + var originTime = (maxTime - minTime) / 60; + var xScale = d3.scaleLinear().range([margins, width - margins]); + xScale.domain([0, originTime]); + var yScale = d3.scaleLinear().range([height - margins, margins]); + yScale.domain([0, topActualScore]); + + graph = d3.select("#graph"); + graph.remove(); + graph = d3.select("#holding").append("svg") + .attr("width", width) + .attr("height", height); + //.attr("style", "background: white"); + + + //graph.append("g") + // .attr("transform", "translate(" + margins + ", 0)") + // .call(d3.axisLeft(yScale)) + // .style("stroke", "white");; + + var maxNumEntry = 10; + //var curEntry = 0; + var winningTeams = []; + for(entry in winners) + { + var curEntry = entry; + if(curEntry >= maxNumEntry) + { + break; + } + entry = teamnames[winners[entry].__hash__]; + winningTeams.push(entry); + //console.log(curEntry); + //console.log(entry); + + //var isTop = false; + //for(var x=0; x < maxNumEntry; x++) + //{ + // var teamhash = reverseTeam[entry]; + // if(winners[x].__hash__ == teamhash) + // { + // curEntry = x; + // isTop = true; + // break; + // } + //} + //if(!isTop) + //{ + // continue; + //} + + var curTeam = teamLines[entry]; + var lastEntry = curTeam[curTeam.length - 1]; + //curTeam.append() + curTeam.push([maxTime, lastEntry[1], lastEntry[2], lastEntry[3], lastEntry]); + var curLayer = graph.append("g"); + curLayer.selectAll("line") + .data(curTeam) + .enter() + .append("line") + .style("stroke", colorScale[curEntry]) + .attr("stroke-width", 4) + .attr("class", "team_" + entry) + .style("z-index", maxNumEntry - curEntry) + .attr("x1", + function(d) + { + return xScale((d[4][0] - minTime) / 60); + }) + .attr("x2", + function(d) + { + return xScale((d[0] - minTime) / 60); + }) + .attr("y1", + function(d) + { + return yScale(d[4][1]); + }) + .attr("y2", + function(d) + { + return yScale(d[1]); + }) + .on("mouseover", handleMouseover) + .on("mouseout", handleMouseout); + + curLayer.selectAll("circle") + .data(curTeam) + .enter() + .append("circle") + .style("fill", colorScale[curEntry]) + .style("z-index", maxNumEntry - curEntry) + .attr("class", "team_" + entry) + .attr("r", 5) + .attr("cx", + function(d) + { + return xScale((d[0] - minTime) / 60); + }) + .attr("cy", + function(d) + { + return yScale(d[1]); + }) + .on("mouseover", handleMouseoverCircle) + .on("mouseout", handleMouseoutCircle); + + curEntry++; + } + + var axisG = graph.append("g"); + axisG + .attr("transform", "translate(0," + (height - margins) + ")") + .call(d3.axisBottom(xScale)); + //.style("stroke", "white"); + axisG.selectAll("path").style("stroke", "white"); + axisG.selectAll("line").style("stroke", "white"); + axisG.selectAll("text").style("fill", "white"); + + graph.append("text") + .attr("text-anchor", "middle") + .attr("transform", "translate(" + (width / 2) + ", " + (height - margins / 8) + ")") + .style("fill", "white") + .text("Time (minutes)"); + + var legend = graph.append("g"); + var legendRowHeight = 40; + legend.selectAll("rect") + .data(winningTeams) + .enter() + .append("rect") + .attr("class", function(d){ return "team_" + d; }) + .attr("fill", function(d, i){ return colorScale[i]; }) + .style("z-index", function(d, i){ return i; }) + .attr("x", margins) + .attr("y", function(d, i){ return margins + legendRowHeight * i; }) + .attr("height", legendRowHeight) + .attr("width", 150) + .on("mouseover", handleMouseoverLegend) + .on("mouseout", handleMouseoutLegend); + + legend.selectAll("text") + .data(winningTeams) + .enter() + .append("text") + //.attr("class", function(d){ return "team_" + d; }) + .attr("fill", "black") + .style("z-index", function(d, i){ return i; }) + .attr("dx", margins) + .attr("dy", function(d, i){ return margins + legendRowHeight * (i + .5); }) + .text(function(d){ return d; }) + .attr("dominant-baseline", "central") + .style("pointer-events", "none"); + + + function handleMouseover(d, i) + { + d3.select("body").selectAll(".tooltip").remove(); + var curClass = d3.select(this).attr("class"); + d3.select("body").selectAll("." + curClass) + .style("stroke", "white") + .style("fill", "white"); + d3.select("body").selectAll("text") + .style("stroke-width", 0); + } + + function handleMouseout(d, i) + { + d3.select("body").selectAll(".tooltip").remove(); + var curClass = d3.select(this).attr("class"); + var zIndex = d3.select(this).style("z-index"); + d3.select("body").selectAll("." + curClass) + .style("stroke", colorScale[maxNumEntry - zIndex]) + .style("fill", colorScale[maxNumEntry - zIndex]); + d3.select("body").selectAll("text") + .style("stroke-width", 0); + } + + var tooltipPadding = 10; + function handleMouseoverCircle(d, i) + { + d3.select("body").selectAll(".tooltip").remove(); + var curClass = d3.select(this).attr("class"); + d3.select("body").selectAll("." + curClass) + .style("stroke", "white") + .style("fill", "white"); + d3.select("body").selectAll("text") + .style("stroke-width", 0); + + graph.append("g").append("text") + .attr("class", "tooltip") + .attr("text-anchor", "middle") + .style("fill", "red") + .style("stroke-width", -4) + .style("stroke", "black") + .style("font-weight", "bolder") + .style("font-size", "large") + .attr("dx", + function() + { + return xScale((d[0] - minTime) / 60); + }) + .attr("dy", + function() + { + return yScale(d[1]) - tooltipPadding; + }) + .text(function(){ return d[2] + " " + d[3]; }) + .style("pointer-events", "none"); + + } + + function handleMouseoutCircle(d, i) + { + d3.select("body").selectAll(".tooltip").remove(); + var curClass = d3.select(this).attr("class"); + var zIndex = d3.select(this).style("z-index"); + d3.select("body").selectAll("." + curClass) + .style("stroke", colorScale[maxNumEntry - zIndex]) + .style("fill", colorScale[maxNumEntry - zIndex]); + d3.select("body").selectAll("text") + .style("stroke-width", 0); + } + + function handleMouseoverLegend(d, i) + { + d3.select("body").selectAll(".tooltip").remove(); + var curClass = d3.select(this).attr("class"); + d3.select("body").selectAll("." + curClass) + .style("stroke", "white") + .style("fill", "white"); + d3.select("body").selectAll("text") + .style("stroke-width", 0); + } + + function handleMouseoutLegend(d, i) + { + d3.select("body").selectAll(".tooltip").remove(); + var curClass = d3.select(this).attr("class"); + var zIndex = d3.select(this).style("z-index"); + d3.select("body").selectAll("." + curClass) + .style("stroke", colorScale[zIndex]) + .style("fill", colorScale[zIndex]); + d3.select("body").selectAll("text") + .style("stroke-width", 0); + } + + + } + else if(mode == "original") + { + // (100 / ncats) * (ncats / topActualScore); + var maxWidth = 100 / topActualScore; + for (var i in winners) { + var team = winners[i]; + var row = document.createElement("div"); + var ncat = 0; + for (var category in highscore) { + var catHigh = highscore[category]; + var catTeam = team[category] || 0; + var catPct = catTeam / catHigh; + var width = maxWidth * catPct; + + var bar = document.createElement("span"); + bar.classList.add("cat" + ncat); + bar.style.width = width + "%"; + bar.textContent = category + ": " + catTeam; + bar.title = bar.textContent; + + row.appendChild(bar); + ncat += 1; + } + + var te = document.createElement("span"); + te.classList.add("teamname"); + te.textContent = teamnames[team.__hash__]; + row.appendChild(te); + + element.appendChild(row); + } + } + if(mode == "total") + { + var colorScale = d3.schemeCategory20; + + var numCats = 0; + for(entry in allQuestions) + { + numCats++; + } + var maxWidth = 100 / (0.0 + numCats); + //console.log(maxWidth); + + for (var i in winners) { + var team = winners[i]; + var row = document.createElement("div"); + var ncat = 0; + for (var category in allQuestions) { + var catHigh = allQuestions[category]; + var catTeam = team[category] || 0; + var catPct = (0.0 + catTeam) / (0.0 + catHigh["total"]); + var width = maxWidth * catPct; + var bar = document.createElement("span"); + + var numLeft = catHigh["total"] - catTeam; + + //bar.classList.add("cat" + ncat); + bar.style.backgroundColor = colorScale[ncat]; + bar.style.color = "white"; + bar.style.width = width + "%"; + bar.textContent = category + ": " + catTeam; + bar.title = bar.textContent; + + row.appendChild(bar); + + ncat++; + + width = maxWidth * (1 - catPct); + if(width > 0) + { + var noBar = document.createElement("span"); + //noBar.classList.add("cat" + ncat); + noBar.style.backgroundColor = colorScale[ncat]; + noBar.style.width = width + "%"; + noBar.textContent = numLeft; + noBar.title = bar.textContent; + + row.appendChild(noBar); + } + ncat += 1; + } + + var te = document.createElement("span"); + te.classList.add("teamname"); + te.textContent = teamnames[team.__hash__]; + row.appendChild(te); + + element.appendChild(row); + } + } + } + + function once() { + loadJSON("points.json", update); + } + if (continuous) { + updateInterval = setInterval(once, interval); + } + once(); +} + diff --git a/www/scoreboard-proj.html b/www/scoreboard-proj.html new file mode 100644 index 0000000..ad32504 --- /dev/null +++ b/www/scoreboard-proj.html @@ -0,0 +1,33 @@ + + + + Scoreboard + + +
+ +
+ + + diff --git a/www/scoreboard.html b/www/scoreboard.html index 6fe1d57..b0a336f 100644 --- a/www/scoreboard.html +++ b/www/scoreboard.html @@ -3,75 +3,18 @@ Scoreboard - - -

Cyber Fire

-

- Scoreboard Type: - Scored Points - All Points - Timeline -

+

Scoreboard

diff --git a/www/scoreboard.js b/www/scoreboard.js index 62b14e8..3f56646 100644 --- a/www/scoreboard.js +++ b/www/scoreboard.js @@ -1,531 +1,164 @@ function loadJSON(url, callback) { - function loaded(e) { - callback(e.target.response); - } - var xhr = new XMLHttpRequest() - xhr.onload = loaded; - xhr.open("GET", url, true); - xhr.responseType = "json"; - xhr.send(); + function loaded(e) { + callback(e.target.response); + } + var xhr = new XMLHttpRequest() + xhr.onload = loaded; + xhr.open("GET", url, true); + xhr.responseType = "json"; + xhr.send(); } -function toObject(arr) { - var rv = {}; - for (var i = 0; i < arr.length; ++i) - if (arr[i] !== undefined) rv[i] = arr[i]; - return rv; +function scoreboardHistoryPush(pointslog) { + let pointsHistory = JSON.parse(localStorage.getItem("pointsHistory")) || []; + if (pointsHistory.length >= 20) { + pointsHistory.shift(); + } + pointsHistory.push(pointslog); + localStorage.setItem("pointsHistory", JSON.stringify(pointsHistory)); } -var updateInterval; +function scoreboard(element, continuous) { + function update(state) { + let teamNames = state["teams"]; + let pointsLog = state["points"]; -function scoreboard(element, continuous, mode, interval) { - if(updateInterval) - { - clearInterval(updateInterval); + // Establish scores, calculate category maximums + let categories = {}; + let maxPointsByCategory = {}; + let totalPointsByTeamByCategory = {}; + for (let entry of pointsLog) { + let entryTimeStamp = entry[0]; + let entryTeamHash = entry[1]; + let entryCategory = entry[2]; + let entryPoints = entry[3]; + + // Populate list of all categories + categories[entryCategory] = entryCategory; + + // Add points to team's points for that category + let points = totalPointsByTeamByCategory[entryTeamHash] || {}; + let categoryPoints = points[entryCategory] || 0; + categoryPoints += entryPoints; + points[entryCategory] = categoryPoints; + totalPointsByTeamByCategory[entryTeamHash] = points; + + // Calculate maximum points scored in each category + let m = maxPointsByCategory[entryCategory] || 0; + maxPointsByCategory[entryCategory] = Math.max(m, categoryPoints); + } + + // Calculate overall scores + let overallScore = {}; + let orderedOverallScores = []; + for (let teamHash in teamNames) { + var score = 0; + for (let cat in categories) { + var catPoints = totalPointsByTeamByCategory[teamHash][cat] || 0; + if (catPoints > 0) { + score += catPoints / maxPointsByCategory[cat]; + } + } + overallScore[teamHash] = score; + orderedOverallScores.push([score, teamHash]); + } + orderedOverallScores.sort(); + orderedOverallScores.reverse(); + + // Clear out the element we're about to populate + while (element.lastChild) { + element.removeChild(element.lastChild); } - function update(state) { - console.log("Updating"); - var teamnames = state["teams"]; - var pointslog = state["points"]; - var highscore = {}; - var teams = {}; - - function pointsCompare(a, b) { - return a[0] - b[0]; - } - pointslog.sort(pointsCompare); - var minTime = pointslog[0][0]; - var maxTime = pointslog[pointslog.length - 1][0]; - var allQuestions = {}; - - for (var i in pointslog) - { - var entry = pointslog[i]; - var timestamp = entry[0]; - var teamhash = entry[1]; - var category = entry[2]; - var points = entry[3]; - - var catPoints = {}; - if(category in allQuestions) - { - catPoints = allQuestions[category]; - } - else - { - catPoints["total"] = 0; - } - - if(!(points in catPoints)) - { - catPoints[points] = 1; - catPoints["total"] = catPoints["total"] + points; - } - else - { - catPoints[points] = catPoints[points] + 1; - } - - allQuestions[category] = catPoints; - } + // Set up scoreboard structure + let spansByTeamByCategory = {}; + for (let pair of orderedOverallScores) { + let teamHash = pair[1]; + let teamName = teamNames[teamHash]; + let teamRow = document.createElement("div"); + let ncat = 0; + spansByTeamByCategory[teamHash] = {}; + for (let cat in categories) { + let catSpan = document.createElement("span"); + catSpan.classList.add("cat" + ncat); + catSpan.style.width = "0%"; + catSpan.textContent = cat + ": 0"; - // Dole out points - for (var i in pointslog) { - var entry = pointslog[i]; - var timestamp = entry[0]; - var teamhash = entry[1]; - var category = entry[2]; - var points = entry[3]; - - var team = teams[teamhash] || {__hash__: teamhash}; - - // Add points to team's points for that category - team[category] = (team[category] || 0) + points; - - // Record highest score in a category - highscore[category] = Math.max(highscore[category] || 0, team[category]); - - teams[teamhash] = team; - } - - // Sort by team score - function teamScore(t) { - var score = 0; + spansByTeamByCategory[teamHash][cat] = catSpan; - for (var category in highscore) { - score += (t[category] || 0) / highscore[category]; - } - // XXX: This function really shouldn't have side effects. - t.__score__ = score; - return score; - } - function pointScore(points, category) - { - return points / highscore[category] - } - function teamCompare(a, b) { - return teamScore(a) - teamScore(b); - } - - var winners = []; - for (var i in teams) { - winners.push(teams[i]); - } - if (winners.length == 0) { - // No teams! - return; - } - winners.sort(teamCompare); - winners.reverse(); - - // Clear out the element we're about to populate - while (element.lastChild) { - element.removeChild(element.lastChild); - } - - // Populate! - var topActualScore = winners[0].__score__; - + teamRow.appendChild(catSpan); + ncat += 1; + } - if(mode == "time") - { - var colorScale = d3.schemeCategory10; - - var teamLines = {}; - var reverseTeam = {}; - for(var i in pointslog) - { - var entry = pointslog[i]; - var timestamp = entry[0]; - var teamhash = entry[1]; - var category = entry[2]; - var points = entry[3]; - var teamname = teamnames[teamhash]; - reverseTeam[teamname] = teamhash; - points = pointScore(points, category); + var te = document.createElement("span"); + te.classList.add("teamname"); + te.textContent = teamName; + teamRow.appendChild(te); - if(!(teamname in teamLines)) - { - var teamHistory = [[timestamp, points, category, entry[3], [minTime, 0, category, 0]]]; - teamLines[teamname] = teamHistory; - } - else - { - var teamHistory = teamLines[teamname]; - teamHistory.push([timestamp, points + teamHistory[teamHistory.length - 1][1], category, entry[3], teamHistory[teamHistory.length - 1]]); - } - } - - //console.log(teamLines); - - var graph = document.createElement("svg"); - graph.id = "graph"; - graph.style.width="90%"; - graph.style.height="40em"; - graph.style.backgroundColor = "white"; - graph.style.display = "table"; - var holdingDiv = document.createElement("div"); - holdingDiv.align="center"; - holdingDiv.id="holding"; - element.appendChild(holdingDiv); - holdingDiv.appendChild(graph); - - var margins = 40; - - var width = graph.offsetWidth; - var height = graph.offsetHeight; - - //var xScale = d3.scaleLinear().range([minTime, maxTime]); - //var yScale = d3.scaleLinear().range([0, topActualScore]); - var originTime = (maxTime - minTime) / 60; - var xScale = d3.scaleLinear().range([margins, width - margins]); - xScale.domain([0, originTime]); - var yScale = d3.scaleLinear().range([height - margins, margins]); - yScale.domain([0, topActualScore]); - - graph = d3.select("#graph"); - graph.remove(); - graph = d3.select("#holding").append("svg") - .attr("width", width) - .attr("height", height); - //.attr("style", "background: white"); - - - //graph.append("g") - // .attr("transform", "translate(" + margins + ", 0)") - // .call(d3.axisLeft(yScale)) - // .style("stroke", "white");; - - var maxNumEntry = 10; - //var curEntry = 0; - var winningTeams = []; - for(entry in winners) - { - var curEntry = entry; - if(curEntry >= maxNumEntry) - { - break; - } - entry = teamnames[winners[entry].__hash__]; - winningTeams.push(entry); - //console.log(curEntry); - //console.log(entry); - - //var isTop = false; - //for(var x=0; x < maxNumEntry; x++) - //{ - // var teamhash = reverseTeam[entry]; - // if(winners[x].__hash__ == teamhash) - // { - // curEntry = x; - // isTop = true; - // break; - // } - //} - //if(!isTop) - //{ - // continue; - //} - - var curTeam = teamLines[entry]; - var lastEntry = curTeam[curTeam.length - 1]; - //curTeam.append() - curTeam.push([maxTime, lastEntry[1], lastEntry[2], lastEntry[3], lastEntry]); - var curLayer = graph.append("g"); - curLayer.selectAll("line") - .data(curTeam) - .enter() - .append("line") - .style("stroke", colorScale[curEntry]) - .attr("stroke-width", 4) - .attr("class", "team_" + entry) - .style("z-index", maxNumEntry - curEntry) - .attr("x1", - function(d) - { - return xScale((d[4][0] - minTime) / 60); - }) - .attr("x2", - function(d) - { - return xScale((d[0] - minTime) / 60); - }) - .attr("y1", - function(d) - { - return yScale(d[4][1]); - }) - .attr("y2", - function(d) - { - return yScale(d[1]); - }) - .on("mouseover", handleMouseover) - .on("mouseout", handleMouseout); - - curLayer.selectAll("circle") - .data(curTeam) - .enter() - .append("circle") - .style("fill", colorScale[curEntry]) - .style("z-index", maxNumEntry - curEntry) - .attr("class", "team_" + entry) - .attr("r", 5) - .attr("cx", - function(d) - { - return xScale((d[0] - minTime) / 60); - }) - .attr("cy", - function(d) - { - return yScale(d[1]); - }) - .on("mouseover", handleMouseoverCircle) - .on("mouseout", handleMouseoutCircle); - - curEntry++; - } - - var axisG = graph.append("g"); - axisG - .attr("transform", "translate(0," + (height - margins) + ")") - .call(d3.axisBottom(xScale)); - //.style("stroke", "white"); - axisG.selectAll("path").style("stroke", "white"); - axisG.selectAll("line").style("stroke", "white"); - axisG.selectAll("text").style("fill", "white"); - - graph.append("text") - .attr("text-anchor", "middle") - .attr("transform", "translate(" + (width / 2) + ", " + (height - margins / 8) + ")") - .style("fill", "white") - .text("Time (minutes)"); - - var legend = graph.append("g"); - var legendRowHeight = 40; - legend.selectAll("rect") - .data(winningTeams) - .enter() - .append("rect") - .attr("class", function(d){ return "team_" + d; }) - .attr("fill", function(d, i){ return colorScale[i]; }) - .style("z-index", function(d, i){ return i; }) - .attr("x", margins) - .attr("y", function(d, i){ return margins + legendRowHeight * i; }) - .attr("height", legendRowHeight) - .attr("width", 150) - .on("mouseover", handleMouseoverLegend) - .on("mouseout", handleMouseoutLegend); - - legend.selectAll("text") - .data(winningTeams) - .enter() - .append("text") - //.attr("class", function(d){ return "team_" + d; }) - .attr("fill", "black") - .style("z-index", function(d, i){ return i; }) - .attr("dx", margins) - .attr("dy", function(d, i){ return margins + legendRowHeight * (i + .5); }) - .text(function(d){ return d; }) - .attr("dominant-baseline", "central") - .style("pointer-events", "none"); - - - function handleMouseover(d, i) - { - d3.select("body").selectAll(".tooltip").remove(); - var curClass = d3.select(this).attr("class"); - d3.select("body").selectAll("." + curClass) - .style("stroke", "white") - .style("fill", "white"); - d3.select("body").selectAll("text") - .style("stroke-width", 0); - } - - function handleMouseout(d, i) - { - d3.select("body").selectAll(".tooltip").remove(); - var curClass = d3.select(this).attr("class"); - var zIndex = d3.select(this).style("z-index"); - d3.select("body").selectAll("." + curClass) - .style("stroke", colorScale[maxNumEntry - zIndex]) - .style("fill", colorScale[maxNumEntry - zIndex]); - d3.select("body").selectAll("text") - .style("stroke-width", 0); - } - - var tooltipPadding = 10; - function handleMouseoverCircle(d, i) - { - d3.select("body").selectAll(".tooltip").remove(); - var curClass = d3.select(this).attr("class"); - d3.select("body").selectAll("." + curClass) - .style("stroke", "white") - .style("fill", "white"); - d3.select("body").selectAll("text") - .style("stroke-width", 0); - - graph.append("g").append("text") - .attr("class", "tooltip") - .attr("text-anchor", "middle") - .style("fill", "red") - .style("stroke-width", -4) - .style("stroke", "black") - .style("font-weight", "bolder") - .style("font-size", "large") - .attr("dx", - function() - { - return xScale((d[0] - minTime) / 60); - }) - .attr("dy", - function() - { - return yScale(d[1]) - tooltipPadding; - }) - .text(function(){ return d[2] + " " + d[3]; }) - .style("pointer-events", "none"); - - } - - function handleMouseoutCircle(d, i) - { - d3.select("body").selectAll(".tooltip").remove(); - var curClass = d3.select(this).attr("class"); - var zIndex = d3.select(this).style("z-index"); - d3.select("body").selectAll("." + curClass) - .style("stroke", colorScale[maxNumEntry - zIndex]) - .style("fill", colorScale[maxNumEntry - zIndex]); - d3.select("body").selectAll("text") - .style("stroke-width", 0); - } - - function handleMouseoverLegend(d, i) - { - d3.select("body").selectAll(".tooltip").remove(); - var curClass = d3.select(this).attr("class"); - d3.select("body").selectAll("." + curClass) - .style("stroke", "white") - .style("fill", "white"); - d3.select("body").selectAll("text") - .style("stroke-width", 0); - } - - function handleMouseoutLegend(d, i) - { - d3.select("body").selectAll(".tooltip").remove(); - var curClass = d3.select(this).attr("class"); - var zIndex = d3.select(this).style("z-index"); - d3.select("body").selectAll("." + curClass) - .style("stroke", colorScale[zIndex]) - .style("fill", colorScale[zIndex]); - d3.select("body").selectAll("text") - .style("stroke-width", 0); - } - - - } - else if(mode == "original") - { - // (100 / ncats) * (ncats / topActualScore); - var maxWidth = 100 / topActualScore; - for (var i in winners) { - var team = winners[i]; - var row = document.createElement("div"); - var ncat = 0; - for (var category in highscore) { - var catHigh = highscore[category]; - var catTeam = team[category] || 0; - var catPct = catTeam / catHigh; - var width = maxWidth * catPct; - - var bar = document.createElement("span"); - bar.classList.add("cat" + ncat); - bar.style.width = width + "%"; - bar.textContent = category + ": " + catTeam; - bar.title = bar.textContent; - - row.appendChild(bar); - ncat += 1; - } - - var te = document.createElement("span"); - te.classList.add("teamname"); - te.textContent = teamnames[team.__hash__]; - row.appendChild(te); - - element.appendChild(row); - } - } - if(mode == "total") - { - var colorScale = d3.schemeCategory20; - - var numCats = 0; - for(entry in allQuestions) - { - numCats++; - } - var maxWidth = 100 / (0.0 + numCats); - //console.log(maxWidth); - - for (var i in winners) { - var team = winners[i]; - var row = document.createElement("div"); - var ncat = 0; - for (var category in allQuestions) { - var catHigh = allQuestions[category]; - var catTeam = team[category] || 0; - var catPct = (0.0 + catTeam) / (0.0 + catHigh["total"]); - var width = maxWidth * catPct; - var bar = document.createElement("span"); - - var numLeft = catHigh["total"] - catTeam; - - //bar.classList.add("cat" + ncat); - bar.style.backgroundColor = colorScale[ncat]; - bar.style.color = "white"; - bar.style.width = width + "%"; - bar.textContent = category + ": " + catTeam; - bar.title = bar.textContent; - - row.appendChild(bar); - - ncat++; - - width = maxWidth * (1 - catPct); - if(width > 0) - { - var noBar = document.createElement("span"); - //noBar.classList.add("cat" + ncat); - noBar.style.backgroundColor = colorScale[ncat]; - noBar.style.width = width + "%"; - noBar.textContent = numLeft; - noBar.title = bar.textContent; - - row.appendChild(noBar); - } - ncat += 1; - } - - var te = document.createElement("span"); - te.classList.add("teamname"); - te.textContent = teamnames[team.__hash__]; - row.appendChild(te); - - element.appendChild(row); - } - } + element.appendChild(teamRow); } - - function once() { - loadJSON("points.json", update); + + // How many categories are there? + var numCategories = 0; + for (var cat in categories) { + numCategories += 1; } - if (continuous) { - updateInterval = setInterval(once, interval); + + // Replay points log, displaying scoreboard at each step + let replayTimer = null; + let replayIndex = 0; + function replayStep(event) { + if (replayIndex > pointsLog.length) { + clearInterval(replayTimer); + return; + } + + // Replay log up until replayIndex + let totalPointsByTeamByCategory = {}; + for (let index = 0; index < replayIndex; index += 1) { + let entry = pointsLog[index]; + let entryTimeStamp = entry[0]; + let entryTeamHash = entry[1]; + let entryCategory = entry[2]; + let entryPoints = entry[3]; + + // Add points to team's points for that category + let points = totalPointsByTeamByCategory[entryTeamHash] || {}; + let categoryPoints = points[entryCategory] || 0; + categoryPoints += entryPoints; + points[entryCategory] = categoryPoints; + totalPointsByTeamByCategory[entryTeamHash] = points; + } + + // Figure out everybody's score + for (let teamHash in teamNames) { + for (let cat in categories) { + let totalPointsByCategory = totalPointsByTeamByCategory[teamHash] || {}; + let points = totalPointsByCategory[cat] || 0; + if (points > 0) { + let score = points / maxPointsByCategory[cat]; + let span = spansByTeamByCategory[teamHash][cat]; + let width = (100.0 / numCategories) * score; + + span.style.width = width + "%"; + span.textContent = cat + ": " + points; + span.title = span.textContent; + } + } + } + + replayIndex += 1; } - once(); + replayStep(); + replayTimer = setInterval(replayStep, 20); + } + + function once() { + loadJSON("points.json", update); + } + if (continuous) { + setInterval(once, 60000); + } + once(); }