diff --git a/res/scoreboard-all.html b/res/scoreboard-all.html new file mode 100644 index 0000000..dd870d6 --- /dev/null +++ b/res/scoreboard-all.html @@ -0,0 +1,50 @@ + + + + Scoreboard + + + + + + + +

Scoreboard

+
+ + + diff --git a/res/scoreboard-proj.html b/res/scoreboard-proj.html new file mode 100644 index 0000000..2d5cf97 --- /dev/null +++ b/res/scoreboard-proj.html @@ -0,0 +1,33 @@ + + + + Scoreboard + + +
+ +
+ + + diff --git a/res/scoreboard-timeline.html b/res/scoreboard-timeline.html new file mode 100644 index 0000000..4de0fdf --- /dev/null +++ b/res/scoreboard-timeline.html @@ -0,0 +1,72 @@ + + + + Scoreboard + + + + + + + + + + +

Scoreboard

+
+ + diff --git a/res/scoreboard.js b/res/scoreboard.js new file mode 100644 index 0000000..7546522 --- /dev/null +++ b/res/scoreboard.js @@ -0,0 +1,528 @@ +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.schemeCategory20; + + 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="100%"; + graph.style.height = "100vh"; + var titleHeight = document.getElementById("title").clientHeight; + titleHeight += document.getElementById("title").offsetTop * 2; + graph.style.backgroundColor = "white"; + graph.style.display = "table"; + var holdingDiv = document.createElement("div"); + holdingDiv.align="center"; + holdingDiv.id="holding"; + holdingDiv.style.height = "100%"; + element.style.height = "100%"; + element.appendChild(holdingDiv); + holdingDiv.appendChild(graph); + + var margins = 40; + var marginsX = 120; + + var width = graph.offsetWidth; + var height = graph.offsetHeight; + height = height - titleHeight - margins; + + //var xScale = d3.scaleLinear().range([minTime, maxTime]); + //var yScale = d3.scaleLinear().range([0, topActualScore]); + var originTime = (maxTime - minTime) / 60; + var xScale = d3.scaleLinear().range([marginsX, 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 * 2]) + .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 * 2]) + .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 = (height - margins) / 10; + legend.selectAll("rect") + .data(winningTeams) + .enter() + .append("rect") + .attr("class", function(d){ return "team_" + d; }) + .attr("fill", function(d, i){ return colorScale[i * 2]; }) + .style("z-index", function(d, i){ return i; }) + .attr("x", 0) + .attr("y", function(d, i){ return legendRowHeight * i; }) + .attr("height", legendRowHeight) + .attr("width", marginsX) + .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", 0) + .attr("dy", function(d, i){ return legendRowHeight * (i + .5); }) + .text(function(d, i){ return i + ": " + d; }) + .attr("dominant-baseline", "central") + .style("pointer-events", "none"); + + //legend.append("g").selectAll("text") + // .data(winningTeams) + // .enter() + // .append("text") + // .attr("class", function(d){ return "team_" + d; }) + // .attr("fill", function(d, i){ return colorScale[i]; }) + // .style("z-index", function(d, i){ return i; }) + // .attr("dx", margins) + // .attr("dy", function(d, i){ return margins + legendRowHeight * (i); }) + // .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) * 2]) + .style("fill", colorScale[(maxNumEntry - zIndex) * 2]); + legend.selectAll("." + curClass) + .style("stroke", colorScale[(maxNumEntry - zIndex) * 2]) + .style("fill", colorScale[(maxNumEntry - zIndex) * 2]); + 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) * 2]) + .style("fill", colorScale[(maxNumEntry - zIndex) * 2]); + legend.selectAll("." + curClass) + .style("stroke", colorScale[(maxNumEntry - zIndex) * 2]) + .style("fill", colorScale[(maxNumEntry - zIndex) * 2]); + 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 * 2]) + .style("fill", colorScale[zIndex * 2]); + legend.selectAll("." + curClass) + .style("stroke", colorScale[(zIndex) * 2]) + .style("fill", colorScale[(zIndex) * 2]); + 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 = Math.floor(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 = highscore[category]; + var catTeam = team[category] || 0; + var catPct = 0; + if (catHigh > 30000) { + catPct = (0.0 + Math.log(1+catTeam)) / (0.0 + Math.log(1+catHigh)); + } else { + catPct = (0.0 + catTeam) / (0.0 + catHigh); + } + var width = maxWidth * catPct; + var bar = document.createElement("span"); + + var numLeft = catHigh - catTeam; + + //bar.classList.add("cat" + ncat); + bar.style.backgroundColor = colorScale[ncat % 20]; + 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 % 20]; + 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(); +}