Added a timeline view to the scoreboard

This commit is contained in:
osboxes.org 2017-11-12 18:24:49 -05:00
parent 04012252f6
commit df36cd9bec
3 changed files with 17601 additions and 155 deletions

17020
www/d3.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -3,18 +3,76 @@
<head> <head>
<title>Scoreboard</title> <title>Scoreboard</title>
<link rel="stylesheet" href="style.css" type="text/css"> <link rel="stylesheet" href="style.css" type="text/css">
<style>
#info {
max-width: 20em;
position: absolute;
opacity: 0.8;
bottom: 1em;
right: 10em;
text-align: center;
}
</style>
<script src="d3.js" async></script>
<script src="scoreboard.js" async></script> <script src="scoreboard.js" async></script>
<script> <script>
function init() {
var sb = document.getElementById("scoreboard"); var type = "original";
scoreboard(sb, true); var interval = 60000;
function preinit()
{
var url = location.href;
url = new URL(url);
type = url.searchParams.get("type");
interval = url.searchParams.get("interval");
if(!type)
{
type = "original";
}
if(!interval)
{
interval = 60000;
}
if(type == "original")
{
document.getElementById("originalButton").checked = true;
}
else if(type == "total")
{
document.getElementById("totalButton").checked = true;
}
else if(type == "time")
{
document.getElementById("timelineButton").checked = true;
}
} }
function updateType(newType)
{
type = newType;
init();
}
function init() {
var sb = document.getElementById("scoreboard");
scoreboard(sb, true, type, interval);
}
window.addEventListener("load", preinit);
window.addEventListener("load", init); window.addEventListener("load", init);
</script> </script>
</head> </head>
<body> <body>
<h1>Scoreboard</h1> <h1>Cyber Fire</h1>
<h2>
Scoreboard Type:
<input type="radio" name="type" id="originalButton" value="original" onclick="updateType('original');"> Scored Points
<input type="radio" name="type" id="totalButton" value="total" onclick="updateType('total');"> All Points
<input type="radio" name="type" id="timelineButton" value="timeline" onclick="updateType('time');"> Timeline
</h2>
<div id="scoreboard"></div> <div id="scoreboard"></div>
</body> </body>
</html> </html>

View File

@ -1,163 +1,531 @@
function loadJSON(url, callback) { function loadJSON(url, callback) {
function loaded(e) { function loaded(e) {
callback(e.target.response); callback(e.target.response);
} }
var xhr = new XMLHttpRequest() var xhr = new XMLHttpRequest()
xhr.onload = loaded; xhr.onload = loaded;
xhr.open("GET", url, true); xhr.open("GET", url, true);
xhr.responseType = "json"; xhr.responseType = "json";
xhr.send(); xhr.send();
} }
function scoreboardHistoryPush(pointslog) { function toObject(arr) {
let pointsHistory = JSON.parse(localStorage.getItem("pointsHistory")) || []; var rv = {};
if (pointsHistory.length >= 20) { for (var i = 0; i < arr.length; ++i)
pointsHistory.shift(); if (arr[i] !== undefined) rv[i] = arr[i];
} return rv;
pointsHistory.push(pointslog);
localStorage.setItem("pointsHistory", JSON.stringify(pointsHistory));
} }
function scoreboard(element, continuous) { var updateInterval;
function update(state) {
let teamNames = state["teams"];
let pointsLog = state["points"];
// Establish scores, calculate category maximums function scoreboard(element, continuous, mode, interval) {
let categories = {}; if(updateInterval)
let maxPointsByCategory = {}; {
let totalPointsByTeamByCategory = {}; clearInterval(updateInterval);
for (let entry of pointsLog) { }
let entryTimeStamp = entry[0]; function update(state) {
let entryTeamHash = entry[1]; console.log("Updating");
let entryCategory = entry[2]; var teamnames = state["teams"];
let entryPoints = entry[3]; var pointslog = state["points"];
var highscore = {};
var teams = {};
// Populate list of all categories function pointsCompare(a, b) {
categories[entryCategory] = entryCategory; return a[0] - b[0];
// 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];
} }
} pointslog.sort(pointsCompare);
overallScore[teamHash] = score; var minTime = pointslog[0][0];
orderedOverallScores.push([score, teamHash]); var maxTime = pointslog[pointslog.length - 1][0];
}
orderedOverallScores.sort();
orderedOverallScores.reverse();
// Clear out the element we're about to populate var allQuestions = {};
while (element.lastChild) {
element.removeChild(element.lastChild);
}
// Set up scoreboard structure for (var i in pointslog)
let spansByTeamByCategory = {}; {
for (let pair of orderedOverallScores) { var entry = pointslog[i];
let teamHash = pair[1]; var timestamp = entry[0];
let teamName = teamNames[teamHash]; var teamhash = entry[1];
let teamRow = document.createElement("div"); var category = entry[2];
let ncat = 0; var points = entry[3];
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";
spansByTeamByCategory[teamHash][cat] = catSpan; var catPoints = {};
if(category in allQuestions)
{
catPoints = allQuestions[category];
}
else
{
catPoints["total"] = 0;
}
teamRow.appendChild(catSpan); if(!(points in catPoints))
ncat += 1; {
} catPoints[points] = 1;
catPoints["total"] = catPoints["total"] + points;
}
else
{
catPoints[points] = catPoints[points] + 1;
}
var te = document.createElement("span"); allQuestions[category] = catPoints;
te.classList.add("teamname");
te.textContent = teamName;
teamRow.appendChild(te);
element.appendChild(teamRow);
}
// How many categories are there?
var numCategories = 0;
for (var cat in categories) {
numCategories += 1;
}
// 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; // 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);
}
}
} }
replayStep();
replayTimer = setInterval(replayStep, 20);
}
function once() { function once() {
loadJSON("points.json", update); loadJSON("points.json", update);
} }
if (continuous) { if (continuous) {
setInterval(once, 60000); updateInterval = setInterval(once, interval);
} }
once(); once();
} }