Skip to content

Instantly share code, notes, and snippets.

@qurben
Last active December 9, 2022 13:34
Show Gist options
  • Save qurben/7a30a46fc2532be9de2f01f0d3ad6861 to your computer and use it in GitHub Desktop.
Save qurben/7a30a46fc2532be9de2f01f0d3ad6861 to your computer and use it in GitHub Desktop.
Advent of Code extension as a single script
// Taken from https://github.com/jeroenheijmans/advent-of-code-charts
(() => {
const loadScript = function() {
let cache = {};
return function(src) {
return cache[src] || (cache[src] = new Promise((resolve, reject) => {
let s = document.createElement('script');
s.defer = true;
s.src = src;
s.onload = resolve;
s.onerror = reject;
document.head.append(s);
}));
}
}();
Promise.all([
loadScript("https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"),
loadScript("https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js")
]).then(() => {
(function(aoc) {
// Based on https://stackoverflow.com/a/38493678/419956 by @user6586783
Chart.pluginService.register({
beforeDraw: function(chart, easing) {
if (chart.config.options.chartArea && chart.config.options.chartArea.backgroundColor) {
var ctx = chart.chart.ctx;
var chartArea = chart.chartArea;
ctx.save();
ctx.fillStyle = chart.config.options.chartArea.backgroundColor;
ctx.fillRect(chartArea.left, chartArea.top, chartArea.right - chartArea.left, chartArea.bottom - chartArea.top);
ctx.restore();
}
}
});
const aocColors = {
"main": "rgba(200, 200, 200, 0.9)",
"secondary": "rgba(150, 150, 150, 0.9)",
"tertiary": "rgba(100, 100, 100, 0.5)",
};
const graphColorStyles = [
"Original (1)",
"Rainbow Alphabetic (2)",
"Rainbow Score (3)",
"Fire Alphabetic (4)",
"Fire Score (5)"
];
const podiumLength = 3;
function range(from, to) {
return [...Array(to - from).keys()].map(k => k + from);
}
function colorWithOpacity(color, alpha) {
if (color.includes("#")) {
return Chart.helpers.color(color).alpha(alpha).rgbString();
} else if (color.includes("hsl")) {
return `${color.slice(0, -1)}, ${alpha})`;
} else {
return color;
}
}
function starSorter(a, b) {
return a.getStarMoment.utc().diff(b.getStarMoment.utc());
}
function getPalette(n, rainbow, original) {
if (original) {
const basePalette = ["#781c81", "#6e1980", "#65187f", "#5e187e", "#58197e", "#531b7f", "#4f1d81", "#4c2182", "#492484", "#462987", "#442d8a", "#43328d", "#423791", "#413d94", "#404298", "#3f489c", "#3f4ea0", "#3f53a5", "#3f59a9", "#3f5fad", "#4064b1", "#4069b5", "#416fb8", "#4274bb", "#4379be", "#447dc0", "#4582c1", "#4686c2", "#488ac2", "#4a8ec1", "#4b92c0", "#4d95be", "#4f99bb", "#519cb8", "#549fb4", "#56a2b0", "#58a4ac", "#5ba7a7", "#5ea9a2", "#60ab9d", "#63ad98", "#66af93", "#69b18e", "#6cb289", "#70b484", "#73b580", "#77b67b", "#7ab877", "#7eb973", "#82ba6f", "#85ba6b", "#89bb68", "#8dbc65", "#91bd61", "#95bd5e", "#99bd5c", "#9dbe59", "#a1be56", "#a5be54", "#a9be52", "#adbe50", "#b1be4e", "#b5bd4c", "#b9bd4a", "#bcbc48", "#c0bb47", "#c3ba45", "#c7b944", "#cab843", "#cdb641", "#d0b540", "#d3b33f", "#d6b13e", "#d8ae3d", "#dbab3c", "#dda93b", "#dfa53a", "#e0a239", "#e29e38", "#e39a37", "#e49636", "#e59235", "#e68d34", "#e78833", "#e78332", "#e77d31", "#e77730", "#e7712f", "#e66b2d", "#e6642c", "#e55e2b", "#e4572a", "#e35029", "#e24928", "#e14226", "#df3b25", "#de3424", "#dc2e22", "#db2721", "#d92120"];
let step = basePalette.length / n;
return [...Array(n).keys()].map(i => basePalette[Math.floor(i * step, 0)]);
}
if (rainbow)
// Dynamic rainbow palette using hsl()
return [...Array(n).keys()].map(i => "hsl(" + i * 300 / n + ", 100%, 50%)");
// Dynamic fire palette red->yellow->green using hsl()
return [...Array(n).keys()].map(i => "hsl(" + i * 120 / n + ", 100%, 50%)")
}
function adjustPoinstFor(year, dayKey, starKey, basePoints) {
// https://github.com/jeroenheijmans/advent-of-code-charts/issues/18
if (year === 2018 && dayKey === "6") {
return 0;
}
if (year === 2020 && dayKey === "1") {
return 0;
}
return basePoints;
}
function transformRawAocJson(json) {
let stars = [];
let year = parseInt(json.event);
let n_members = Object.keys(json.members).length;
let members = Object.keys(json.members)
.map(k => json.members[k])
.map(m => {
let i = 0;
m.stars = [];
m.name = m.name || `(anonymous user ${m.id})`;
m.podiumStars = [];
for (let dayKey of Object.keys(m.completion_day_level)) {
for (let starKey of Object.keys(m.completion_day_level[dayKey])) {
let starMoment = moment.unix(m.completion_day_level[dayKey][starKey].get_star_ts).utc();
let star = {
memberId: m.id,
dayNr: parseInt(dayKey, 10),
dayKey: dayKey,
starNr: parseInt(starKey, 10),
starKey: starKey,
getStarDay: parseInt(`${dayKey}.${starKey}`, 10),
getStarTimestamp: m.completion_day_level[dayKey][starKey].get_star_ts,
getStarMoment: starMoment,
timeTaken: null, // adding this later on, which is easier :D
timeTakenSeconds: null, // adding this later on as well
};
stars.push(star);
m.stars.push(star);
}
}
m.stars = m.stars.sort(starSorter);
m.stars.forEach((s, idx) => {
s.nrOfStarsAfterThisOne = idx + 1;
let startOfDay = moment.utc([year, 11, s.dayNr, 5, 0, 0]); // AoC starts at 05:00 UTC
s.timeTaken = s.getStarMoment.diff(startOfDay, "minutes");
s.timeTakenSeconds = s.getStarMoment.diff(startOfDay, "seconds");
});
return m;
})
.filter(m => m.stars.length > 0)
.sort((a, b) => a.name.localeCompare(b.name));
let allMoments = stars.map(s => s.getStarMoment).concat([moment("" + year + "-12-25T00:00:00-0000")]);
let maxMoment = moment.min([moment.max(allMoments), moment("" + year + "-12-31T00:00:00-0000")]);
let availablePoints = {};
for (let i = 1; i <= 25; i++) {
availablePoints[i] = {};
for (let j = 1; j <= 2; j++) {
availablePoints[i][j] = n_members;
}
}
let orderedStars = stars.sort(starSorter);
for (let star of orderedStars) {
const basePoints = availablePoints[star.dayKey][star.starKey]--;
star.points = adjustPoinstFor(year, star.dayKey, star.starKey, basePoints);
star.rank = n_members - basePoints + 1;
}
for (let m of members) {
let accumulatedPoints = 0;
for (let s of m.stars.sort(starSorter)) {
accumulatedPoints += s.points;
s.nrOfPointsAfterThisOne = accumulatedPoints;
m.score = accumulatedPoints;
}
}
let maxDay = Math.max.apply(Math, stars.filter(s => s.starNr === 2).map(s => s.dayNr))
let days = {};
for (let d = 1; d <= maxDay; d++) {
days[d] = {
dayNr: d,
podium: stars.filter(s => s.dayNr === d && s.starNr === 2).sort(starSorter),
podiumFirstPuzzle: stars.filter(s => s.dayNr === d && s.starNr === 1).sort(starSorter),
};
for (let i = 0; i < days[d].podium.length; i++) {
days[d].podium[i].awardedPodiumPlace = i;
days[d].podiumFirstPuzzle[i].awardedPodiumPlaceFirstPuzzle = i;
}
}
for (let m of members) {
m.podiumPlacesPerDay = getPodiumFor(m);
m.podiumPlacesPerDayFirstPuzzle = getPodiumForFirstPuzzle(m);
}
let curGraphColorStyle = (getCurrentGraphColorStyle() || "").toLowerCase();
let isOriginal = curGraphColorStyle.includes("original");
let isRainbow = curGraphColorStyle.includes("rainbow");
let orderByScore = curGraphColorStyle.includes("score");
let colors = getPalette(members.length, isRainbow, isOriginal);
if (orderByScore)
members.sort((a, b) => b.score - a.score).forEach((m, idx) => m.color = colors[idx]);
else
members.forEach((m, idx) => m.color = colors[idx]);
return {
maxDay: maxDay,
maxMoment: maxMoment,
days: days,
stars: stars,
members: members,
year: year,
n_members: n_members,
};
}
function getPodiumFor(member) {
let medals = [];
for (let p = 0; p < podiumLength; p++) {
medals.push(member.stars.filter(s => s.awardedPodiumPlace === p).length);
}
return medals;
}
function getPodiumForFirstPuzzle(member) {
let medals = [];
for (let p = 0; p < podiumLength; p++) {
medals.push(member.stars.filter(s => s.awardedPodiumPlaceFirstPuzzle === p).length);
}
return medals;
}
function memberByPodiumSorter(a, b) {
let aMedals = getPodiumFor(a);
let bMedals = getPodiumFor(b);
for (let i = 0; i < aMedals.length; i++) {
if (aMedals[i] !== bMedals[i]) {
return bMedals[i] - aMedals[i];
}
}
aMedals = getPodiumForFirstPuzzle(a);
bMedals = getPodiumForFirstPuzzle(b);
for (let i = 0; i < aMedals.length; i++) {
if (aMedals[i] !== bMedals[i]) {
return bMedals[i] - aMedals[i];
}
}
return b.local_score - a.local_score;
}
function getCacheKey() {
return `aoc-data-v1-${document.location.pathname}`;
}
function getCache() {
console.info("Getting cache", getCacheKey());
return JSON.parse(localStorage.getItem(getCacheKey()));
}
function updateCache(data) {
console.log("Updating cache");
localStorage.setItem(getCacheKey(), JSON.stringify({
data: data,
timestamp: Date.now()
}));
return data;
}
function clearCache() {
console.log("Clearing cache", getCacheKey());
localStorage.setItem(getCacheKey(), null);
}
function toggleShowAll() {
localStorage.setItem("aoc-flag-v1-show-all", !isShowAllToggled());
location.reload();
}
function isShowAllToggled() {
return !!JSON.parse(localStorage.getItem("aoc-flag-v1-show-all"));
}
function toggleResponsiveness() {
localStorage.setItem("aoc-flag-v1-is-responsive", !isResponsivenessToggled());
location.reload();
}
function isResponsivenessToggled() {
return !!JSON.parse(localStorage.getItem("aoc-flag-v1-is-responsive"));
}
function getCurrentGraphColorStyle() {
return localStorage.getItem("aoc-flag-v1-color-style");
}
function toggleCurrentGraphColorStyle() {
let cur = graphColorStyles.indexOf(getCurrentGraphColorStyle());
localStorage.setItem("aoc-flag-v1-color-style", graphColorStyles[(cur + 1) % graphColorStyles.length]);
location.reload();
}
function setDisplayDay(dayNumber) {
localStorage.setItem("aoc-flag-v1-display-day", dayNumber);
}
function getDisplayDay() {
let value = localStorage.getItem("aoc-flag-v1-display-day");
return value;
}
function setTimeTableSort(sort) {
localStorage.setItem("aoc-flag-v1-delta-sort", sort);
location.reload();
}
function getTimeTableSort() {
let value = localStorage.getItem("aoc-flag-v1-delta-sort") || "delta";
return value;
}
let prevClick;
function isDoubleClick() {
let now = new Date();
if (!prevClick) {
prevClick = now;
return false;
}
let diff = now - prevClick;
prevClick = now;
return diff < 300;
}
function legendOnClick(e, li) {
// always do default click behavior
Chart.defaults.global.legend.onClick.apply(this, [e, li]);
if (isDoubleClick()) {
let chart = this.chart;
// always show doubleclicked item
chart.getDatasetMeta(li.datasetIndex).hidden = null;
// count how many hidden datasets are there
let hiddenCount = chart.data.datasets
.map((_, dataSetIndex) => chart.getDatasetMeta(dataSetIndex))
.filter(meta => meta.hidden)
.length;
// deciding to invert items 'hidden' state depending
// if they are already mostly hidden
let hide = (hiddenCount >= (chart.data.datasets.length - 1) * 0.5) ? null : true;
chart.data.datasets.forEach((_, dataSetIndex) => {
if (dataSetIndex === li.datasetIndex) {
return;
}
let dsMeta = chart.getDatasetMeta(dataSetIndex);
dsMeta.hidden = hide;
});
chart.update();
}
}
function formatTimeTaken(seconds) {
if (seconds > 24 * 3600) {
return ">24h"
}
return moment().startOf('day').seconds(seconds).format('HH:mm:ss')
}
function formatStarMomentForTitle(memberStar) {
return memberStar.getStarMoment.local().format("HH:mm:ss YYYY-MM-DD") + " (local time)";
}
function getLeaderboardJson() {
// 1. Check if dummy data was loaded...
if (!!aoc.dummyData) {
console.info("Loading dummyData");
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(transformRawAocJson(aoc.dummyData));
});
}, 100);
}
// 2. Apparently we can use real calls...
else {
let anchor = document.querySelector("#api_info a");
if (!!anchor) {
let url = anchor.href;
const cache = getCache();
console.info("Found cache", cache);
if (cache) {
const ttl = new Date(cache.timestamp + (5 * 60 * 1000));
console.info("Found cached value valid until", ttl);
if (Date.now() < ttl) {
console.info("Cache was still valid!");
return Promise.resolve(cache.data)
.then(json => transformRawAocJson(json));
}
}
console.info(`Loading data from url ${url}`);
return fetch(url, {
credentials: "same-origin"
})
.then(data => data.json())
.then(json => updateCache(json))
.then(json => transformRawAocJson(json));
} else {
console.info("Could not find anchor to JSON feed, assuming no charts can be plotted here.");
return new Promise((resolve, reject) => {});
}
}
}
class App {
constructor() {
console.info("Constructing App");
this.wrapper = document.body.appendChild(document.createElement("div"));
this.controls = this.wrapper.appendChild(document.createElement("div"));
this.medals = this.wrapper.appendChild(document.createElement("div"));
this.perDayLeaderBoard = this.wrapper.appendChild(document.createElement("div"));
this.graphs = this.wrapper.appendChild(document.createElement("div"));
this.graphs.style.display = "flex";
this.graphs.style.flexWrap = "wrap";
this.graphs.style.flexDirection = "row";
if (!getCurrentGraphColorStyle())
toggleCurrentGraphColorStyle();
getLeaderboardJson()
.then(data => this.loadCacheBustingButton(data))
.then(data => this.loadMedalOverview(data))
.then(data => this.loadPerDayLeaderBoard(data))
.then(data => this.loadPointsOverTime(data))
.then(data => this.loadStarsOverTime(data))
.then(data => this.loadDayVsTime(data))
.then(data => this.loadTimePerStar(data));
}
loadHr(data) {
this.controls.appendChild(document.createElement("hr"));
return data;
}
loadCacheBustingButton(data) {
const cacheBustLink = this.controls.appendChild(document.createElement("a"));
cacheBustLink.innerText = "🔄 Clear Charts Cache";
cacheBustLink.style.cursor = "pointer";
cacheBustLink.style.background = aocColors.tertiary;
cacheBustLink.style.display = "inline-block";
cacheBustLink.style.padding = "2px 8px";
cacheBustLink.style.border = `1px solid ${aocColors.secondary}`;
cacheBustLink.addEventListener("click", () => clearCache());
const responsiveToggleLink = this.controls.appendChild(document.createElement("a"));
responsiveToggleLink.innerText = (isResponsivenessToggled() ? "✅" : "❌") + " Responsive > 1800px";
responsiveToggleLink.title = "Trigger side-by-side graphs if the viewport is wider than 1800px";
responsiveToggleLink.style.cursor = "pointer";
responsiveToggleLink.style.background = aocColors.tertiary;
responsiveToggleLink.style.display = "inline-block";
responsiveToggleLink.style.padding = "2px 8px";
responsiveToggleLink.style.border = `1px solid ${aocColors.secondary}`;
responsiveToggleLink.style.marginLeft = "8px";
responsiveToggleLink.addEventListener("click", () => toggleResponsiveness());
const colorToggleLink = this.controls.appendChild(document.createElement("a"));
colorToggleLink.innerText = `🎨 Palette: ${getCurrentGraphColorStyle()}`;
colorToggleLink.title = "Cycle through different graph color styles";
colorToggleLink.style.cursor = "pointer";
colorToggleLink.style.background = aocColors.tertiary;
colorToggleLink.style.display = "inline-block";
colorToggleLink.style.padding = "2px 8px";
colorToggleLink.style.border = `1px solid ${aocColors.secondary}`;
colorToggleLink.style.marginLeft = "8px";
colorToggleLink.addEventListener("click", () => toggleCurrentGraphColorStyle());
return data;
}
loadPerDayLeaderBoard(data) {
this.perDayLeaderBoard.title = "Per Day LeaderBoard";
let titleElement = this.perDayLeaderBoard.appendChild(document.createElement("h3"));
titleElement.innerText = "Stats Per Day: ";
titleElement.style.fontFamily = "Source Code Pro, monospace";
titleElement.style.fontWeight = "normal";
titleElement.style.marginTop = "32px";
titleElement.style.marginBottom = "8px";
this.perDayLeaderBoard.style.marginBottom = "32px";
let displayDay = getDisplayDay();
// taking the min to avoid going out of bounds for current year
displayDay = displayDay ? Math.min(parseInt(displayDay), data.maxDay) : data.maxDay;
let tablePerDay = {},
anchorPerDay = {};
for (let d = 1; d <= data.maxDay; ++d) {
let a = titleElement.appendChild(document.createElement("a"));
a.innerText = " " + d.toString();
a.addEventListener("click", () => {
setDisplayDay(d);
setVisible(d);
});
a.style.cursor = "pointer";
anchorPerDay[d] = a;
}
function generateTable(displayDay) {
let gridElement = document.createElement("table");
tablePerDay[displayDay] = gridElement;
gridElement.style.borderCollapse = "collapse";
gridElement.style.fontSize = "16px";
function sortByDeltaTime(a, b) {
let a1 = a.stars.find(s => s.dayNr === displayDay && s.starNr === 1);
let a2 = a.stars.find(s => s.dayNr === displayDay && s.starNr === 2);
let b1 = b.stars.find(s => s.dayNr === displayDay && s.starNr === 1);
let b2 = b.stars.find(s => s.dayNr === displayDay && s.starNr === 2);
if (!a2 && !b2) return 0;
if (!a2) return 1;
if (!b2) return -1;
const aTime = a2.timeTakenSeconds - a1.timeTakenSeconds;
const bTime = b2.timeTakenSeconds - b1.timeTakenSeconds;
if (aTime === bTime) return 0;
return aTime > bTime ? 1 : -1;
}
function sortByPart(starNr) {
return function sortByPart2Time(a, b) {
let aStar = a.stars.find(s => s.dayNr === displayDay && s.starNr === starNr);
let bStar = b.stars.find(s => s.dayNr === displayDay && s.starNr === starNr);
if (!aStar) return 1;
if (!bStar) return -1;
return aStar.timeTakenSeconds > bStar.timeTakenSeconds ? 1 : -1;
}
}
function sortByTotalPoints(a, b) {
let aPoints = a.stars.filter(s => s.dayNr == displayDay).reduce((acc, v) => acc + v.points, 0);
let bPoints = b.stars.filter(s => s.dayNr == displayDay).reduce((acc, v) => acc + v.points, 0);
return bPoints - aPoints;
}
let grid = data.members;
let sortFns = {
"delta": sortByDeltaTime,
"completion": sortByTotalPoints,
"part1": sortByPart(1),
"part2": sortByPart(2),
};
grid.sort(sortFns[Object.keys(sortFns).find(k => k === getTimeTableSort()) || "delta"]);
function createHeaderCell(sorting, text, color = "inherit") {
const td = document.createElement("td");
td.innerText = text;
td.style.padding = "4px 8px";
td.style.color = color;
td.style.textAlign = "center";
td.style.cursor = "pointer";
td.addEventListener("click", () => setTimeTableSort(sorting));
return td;
}
{
// first row header
let tr = gridElement.appendChild(document.createElement("tr"));
let th = tr.appendChild(document.createElement("th"))
th = tr.appendChild(createHeaderCell("part1", "----- Part 1 -----", "#9999cc"));
th.colSpan = 3;
th = tr.appendChild(createHeaderCell("delta", "----- Delta -----"));
th.title = "Everyone starting puzzles at different times? See who's the fastest to go from 1 to 2 stars on a day!";
th.colSpan = 1;
th = tr.appendChild(createHeaderCell("part2", "----- Part 2 -----", "#ffff66"));
th.colSpan = 3;
th = tr.appendChild(createHeaderCell("total", "----- Total -----"));
th.colSpan = 1;
} {
// second row header
let tr = gridElement.appendChild(document.createElement("tr"));
let td = tr.appendChild(document.createElement("td"));
// Part 1
td = tr.appendChild(createHeaderCell("part1", "Time" + (getTimeTableSort() === "part1" ? " ⬇" : ""), "#9999cc"));
if (getTimeTableSort() === "part1") {
td.style.color = "#9999ee";
td.style.textShadow = "0 0 5px #9999cc";
}
td = tr.appendChild(createHeaderCell("part1", "Rank", "#9999cc"));
td = tr.appendChild(createHeaderCell("part1", "Points", "#9999cc"));
// Delta
td = tr.appendChild(createHeaderCell("delta", "Delta Time" + (getTimeTableSort() === "delta" ? " ⬇" : "")));
td.title = "Time difference between Part 2 and Part 1";
if (getTimeTableSort() === "delta") {
td.style.color = "#ffffff";
td.style.textShadow = "0 0 5px #ffffff";
}
// Part 2
td = tr.appendChild(createHeaderCell("part2", "Time" + (getTimeTableSort() === "part2" ? " ⬇" : ""), "#ffff66"));
if (getTimeTableSort() === "part2") {
td.style.color = "#ffff66";
td.style.textShadow = "0 0 5px #ffff66";
}
td = tr.appendChild(createHeaderCell("part2", "Rank", "#ffff66"));
td = tr.appendChild(createHeaderCell("part2", "Points", "#ffff66"));
// Total
td = tr.appendChild(createHeaderCell("completion", "Points" + (getTimeTableSort() === "completion" ? " ⬇" : "")));
if (getTimeTableSort() === "completion") {
td.style.color = "#ffffff";
td.style.textShadow = "0 0 5px #ffffff";
}
}
function createCell(text) {
const td = document.createElement("td");
td.innerText = text;
td.style.border = "1px solid #333";
td.style.padding = "6px";
td.style.textAlign = "center";
return td;
}
const maxSecondsForSparkline = 4 /* hours */ * 3600;
let rank = 0;
let maxDeltaTime = Math.max.apply(Math, grid
.map(m => {
let memberStar1 = m.stars.find(s => s.dayNr === displayDay && s.starNr === 1);
let memberStar2 = m.stars.find(s => s.dayNr === displayDay && s.starNr === 2);
const delta = memberStar2 ? memberStar2.timeTakenSeconds - memberStar1.timeTakenSeconds : null;
return delta > maxSecondsForSparkline ? null : delta;
}));
for (let member of grid) {
let memberStar1 = member.stars.find(s => s.dayNr === displayDay && s.starNr === 1);
let memberStar2 = member.stars.find(s => s.dayNr === displayDay && s.starNr === 2);
// skip users that didn't solve any problem today
if (!memberStar1 && !memberStar2) {
continue;
}
rank += 1;
let tr = gridElement.appendChild(document.createElement("tr"));
let td = tr.appendChild(createCell(rank.toString() + ". " + member.name))
td.style.textAlign = "left";
td = tr.appendChild(createCell((memberStar1 ? formatTimeTaken(memberStar1.timeTakenSeconds) : "")))
td.title = memberStar1 ? formatStarMomentForTitle(memberStar1) : "Star 1 not done yet";
if (getTimeTableSort() === "part1") {
td.style.color = "#ffffff";
td.style.textShadow = "0 0 5px #ffffff";
}
td = tr.appendChild(createCell((memberStar1 ? memberStar1.rank : "")))
td = tr.appendChild(createCell((memberStar1 ? memberStar1.points : "")))
td = tr.appendChild(createCell(memberStar2 ? formatTimeTaken(memberStar2.timeTakenSeconds - memberStar1.timeTakenSeconds) : ""));
if (getTimeTableSort() === "delta") {
td.style.color = "#ffffff";
td.style.textShadow = "0 0 5px #ffffff";
}
if (memberStar2 && maxDeltaTime) {
const delta = memberStar2.timeTakenSeconds - memberStar1.timeTakenSeconds;
const fraction = Math.min(100, delta / maxDeltaTime * 100);
const sparkline = td.appendChild(document.createElement("div"));
sparkline.style.height = "2px";
sparkline.style.marginTop = "4px";
sparkline.style.marginBottom = "1px";
sparkline.style.width = `${fraction}%`;
sparkline.style.backgroundColor = "#ffffff";
if (getTimeTableSort() === "delta") {
sparkline.style.boxShadow = "1px 1px 5px rgba(255, 255, 255, 0.5), -1px -1px 5px rgba(255, 255, 255, 0.5)";
}
sparkline.style.opacity = delta > maxDeltaTime ? "0.15" : "0.75";
sparkline.title = "Spark line showing relative 'delta time' values (up to a maximum delta time)";
}
td = tr.appendChild(createCell((memberStar2 ? formatTimeTaken(memberStar2.timeTakenSeconds) : "")))
td.title = memberStar2 ? formatStarMomentForTitle(memberStar2) : "Star 2 not done yet";
if (getTimeTableSort() === "part2") {
td.style.color = "#ffffff";
td.style.textShadow = "0 0 5px #ffffff";
}
td = tr.appendChild(createCell((memberStar2 ? memberStar2.rank : "")))
td = tr.appendChild(createCell((memberStar2 ? memberStar2.points : "")))
let totalScore = 0;
if (memberStar1) {
totalScore += memberStar1.points;
}
if (memberStar2) {
totalScore += memberStar2.points;
}
td = tr.appendChild(createCell(totalScore ? totalScore : "0"))
if (getTimeTableSort() === "completion") {
td.style.color = "#ffffff";
td.style.textShadow = "0 0 5px #ffffff";
}
}
return gridElement;
}
function setVisible(day) {
for (const t in tablePerDay) {
tablePerDay[t].style.display = "none";
}
tablePerDay[day].style.display = "table";
for (const a in anchorPerDay) {
anchorPerDay[a].style.color = "";
anchorPerDay[a].style.textShadow = "";
}
anchorPerDay[day].style.color = "#ffffff";
anchorPerDay[day].style.textShadow = "0 0 5px #ffffff";
}
for (let i = 1; i <= data.maxDay; i++) {
this.perDayLeaderBoard.appendChild(generateTable(i));
}
setVisible(displayDay);
return data;
}
loadMedalOverview(data) {
const medalHtml = n => n === 0 ? "🥇" : n === 1 ? "🥈" : n === 2 ? "🥉" : `${n}`;
const medalColor = n => n === 0 ? "gold" : n === 1 ? "silver" : n === 2 ? "#945210" : "rgba(15, 15, 35, 1.0)";
this.medals.title =
(isShowAllToggled() ?
'' :
'For each day, the top 3 to get the second star are shown. ') +
'Behind each medal you can get a glimpse of the podium for the *first* star.';
let titleElement = this.medals.appendChild(document.createElement("h3"));
titleElement.innerText = "Podium per day: ";
titleElement.style.fontFamily = "Helvetica, Arial, sans-serif";
titleElement.style.fontWeight = "normal";
titleElement.style.marginBottom = "4px";
const showAllToggleLink = titleElement.appendChild(document.createElement("a"));
showAllToggleLink.innerText = isShowAllToggled() ? "🎄 Showing all participants" : "🥇 Showing only medalists";
showAllToggleLink.title = "Toggle between showing only medalists or all participants";
showAllToggleLink.style.cursor = "pointer";
showAllToggleLink.addEventListener("click", () => toggleShowAll());
let gridElement = document.createElement("table");
gridElement.style.borderCollapse = "collapse";
gridElement.style.fontSize = "16px";
let grid = data.members;
let tr = gridElement.appendChild(document.createElement("tr"));
for (let d = 0; d <= 25; d++) {
let td = tr.appendChild(document.createElement("td"));
td.innerText = d === 0 ? "" : d;
td.align = "center";
}
tr.appendChild(document.createElement("td"));
for (let n = 0; n < podiumLength; n++) {
let td = tr.appendChild(document.createElement("td"));
let span = td.appendChild(document.createElement("span"));
span.innerText = medalHtml(n);
span.style.backgroundColor = medalColor(n);
span.style.padding = "1px";
td.style.padding = "4px";
td.align = "center";
}
for (let member of grid.sort(memberByPodiumSorter)) {
let tr = document.createElement("tr");
let medalCount = 0;
let td = tr.appendChild(document.createElement("td"));
td.innerText = member.name;
td.style.border = "1px solid #333";
td.style.padding = "2px 8px";
for (let d = 1; d <= 25; d++) {
let td = tr.appendChild(document.createElement("td"));
td.style.border = "1px solid #333";
td.style.padding = "3px 4px";
td.style.textAlign = "center";
let div = td.appendChild(document.createElement("div"));
div.style.padding = "2px";
div.style.minWidth = "24px";
div.style.minHeight = "24px";
if (d <= data.maxDay) {
let secondPuzzlePodiumPlace = data.days[d].podium.findIndex(n => n.memberId === member.id);
let firstPuzzlePodiumPlace = data.days[d].podiumFirstPuzzle.findIndex(n => n.memberId === member.id);
if (firstPuzzlePodiumPlace >= 0 && firstPuzzlePodiumPlace < podiumLength) {
div.style.boxShadow = `inset 2px 2px 0 0 ${medalColor(firstPuzzlePodiumPlace)}, inset -2px -2px 0 0 ${medalColor(firstPuzzlePodiumPlace)}`;
medalCount++;
}
let span = div.appendChild(document.createElement("span"));
span.innerText = medalHtml(secondPuzzlePodiumPlace);
span.style.display = "block";
span.style.padding = "1px";
span.style.borderRadius = "2px";
span.style.border = "1px solid #333";
span.style.backgroundColor = medalColor(secondPuzzlePodiumPlace);
let memberStar1 = member.stars.find(s => s.dayNr === d && s.starNr === 1);
let memberStar2 = member.stars.find(s => s.dayNr === d && s.starNr === 2);
td.title = (memberStar1 ? formatStarMomentForTitle(memberStar1) : "Star 1 not done yet") +
"\n" +
(memberStar2 ? formatStarMomentForTitle(memberStar2) : "Star 2 not done yet");
if (secondPuzzlePodiumPlace >= 0 && secondPuzzlePodiumPlace < podiumLength) {
medalCount++;
div.style.opacity = 0.5 + (0.5 * ((podiumLength - secondPuzzlePodiumPlace) / podiumLength));
} else {
span.innerText = secondPuzzlePodiumPlace >= 0 ? (secondPuzzlePodiumPlace + 1) : '\u2003';
span.style.opacity = 0.25;
}
}
}
let separator = tr.appendChild(document.createElement("td"));
separator.innerText = "\u00A0";
for (let n = 0; n < podiumLength; n++) {
let td = tr.appendChild(document.createElement("td"));
td.innerText = member.podiumPlacesPerDay[n];
td.style.border = "1px solid #333";
td.style.padding = "2px 8px";
td.align = "center";
}
if (isShowAllToggled() || medalCount > 0) {
gridElement.appendChild(tr);
}
}
this.medals.appendChild(gridElement);
return data;
}
createGraphCanvas(data, title = "") {
var element = document.createElement("canvas");
if (isResponsivenessToggled()) {
element.style.maxWidth = window.matchMedia("(min-width: 1800px)").matches ? "50%" : "100%";
}
element.title = title;
return element;
}
loadDayVsTime(data) {
let datasets = data.members.map(m => {
return {
label: m.name,
backgroundColor: m.color,
borderWidth: 1,
borderColor: "#000",
pointRadius: 6,
data: m.stars.map(s => {
return {
x: s.dayNr + s.starNr / 2 - 1,
y: s.timeTaken
};
})
};
});
let element = this.createGraphCanvas(data, "Log10 function of the time taken for each user to get the stars");
this.graphs.appendChild(element);
let chart = new Chart(element.getContext("2d"), {
type: "scatter",
data: {
datasets: datasets,
},
options: {
responsive: true,
tooltips: {
callbacks: {
label: (item, _) => {
const day = Math.floor(Number(item.label) + 0.5);
const star = Number(item.label) < day ? 1 : 2;
const mins = item.value;
return `Day ${day} star ${star} took ${mins} minutes to complete`;
},
},
},
chartArea: {
backgroundColor: "rgba(0, 0, 0, 0.25)"
},
legend: {
position: "right",
labels: {
fontColor: aocColors["main"],
},
onClick: legendOnClick
},
title: {
display: true,
text: "Stars vs Log10(minutes taken per star)",
fontSize: 24,
fontStyle: "normal",
fontColor: aocColors["main"],
lineHeight: 2.0,
},
scales: {
xAxes: [{
ticks: {
min: 0,
max: 25,
stepSize: 1,
fontColor: aocColors["main"],
},
scaleLabel: {
display: true,
labelString: "Day of Advent",
fontColor: aocColors["main"],
},
gridLines: {
color: aocColors["tertiary"],
zeroLineColor: aocColors["secondary"],
},
}],
yAxes: [{
type: "logarithmic",
ticks: {
fontColor: aocColors["main"],
},
scaleLabel: {
display: true,
labelString: "minutes taken per star (log scale)",
fontColor: aocColors["main"],
},
gridLines: {
color: aocColors["tertiary"],
zeroLineColor: aocColors["secondary"],
},
}]
}
}
});
return data;
}
loadTimePerStar(data) {
let datasets = [];
let n = Math.min(3, data.members.length);
let relevantMembers = data.members.sort((a, b) => b.score - a.score).slice(0, n);
for (let member of relevantMembers) {
let star1DataSet = {
label: `${member.name} (★)`,
stack: `Stack ${member.name}`,
backgroundColor: member.color,
data: [],
};
let star2DataSet = {
label: `${member.name} (★★)`,
stack: `Stack ${member.name}`,
backgroundColor: colorWithOpacity(member.color, 0.5),
data: [],
};
for (let i = 1; i <= 25; i++) {
let star1 = data.stars.find(s => s.memberId === member.id && s.dayNr === i && s.starKey === "1");
let star2 = data.stars.find(s => s.memberId === member.id && s.dayNr === i && s.starKey === "2");
star1DataSet.data.push(!!star1 ? star1.timeTaken : 0);
star2DataSet.data.push(!!star2 ? star2.timeTaken - star1.timeTaken : 0);
}
// Over 240 minutes? Then just nullify the data, we assume folks didn't try.
for (var i = 0; i < star1DataSet.data.length; i++) {
if (star1DataSet.data[i] + star2DataSet.data[i] > 240) {
if (star1DataSet.data[i] > 240) {
star1DataSet.data[i] = null;
}
star2DataSet.data[i] = null;
}
}
datasets.push(star1DataSet);
datasets.push(star2DataSet);
}
let element = this.createGraphCanvas(data, "From the top players, show the number of minutes taken each day. (Exclude results over 4 hours.)");
this.graphs.appendChild(element);
let chart = new Chart(element.getContext("2d"), {
type: "bar",
data: {
labels: range(1, 26),
datasets: datasets,
},
options: {
responsive: true,
chartArea: {
backgroundColor: "rgba(0, 0, 0, 0.25)"
},
legend: {
position: "right",
labels: {
fontColor: aocColors["main"],
},
onClick: legendOnClick
},
title: {
display: true,
text: `Minutes taken per star - top ${n} players`,
fontSize: 24,
fontStyle: "normal",
fontColor: aocColors["main"],
lineHeight: 2.0,
},
scales: {
xAxes: [{
stacked: true,
ticks: {
fontColor: aocColors["main"],
},
scaleLabel: {
display: true,
labelString: "Day of Advent",
fontColor: aocColors["main"],
},
gridLines: {
color: aocColors["tertiary"],
zeroLineColor: aocColors["secondary"],
},
}],
yAxes: [{
stacked: true,
ticks: {
fontColor: aocColors["main"],
},
scaleLabel: {
display: true,
labelString: "minutes taken per star",
fontColor: aocColors["main"],
},
gridLines: {
color: aocColors["tertiary"],
zeroLineColor: aocColors["secondary"],
},
}],
}
}
});
return data;
}
loadPointsOverTime(data) {
let datasets = data.members.sort((a, b) => a.name.localeCompare(b.name)).map(m => {
return {
label: m.name,
lineTension: 0.2,
fill: false,
borderWidth: 1.5,
borderColor: m.color,
backgroundColor: m.color,
data: m.stars.filter(s => s.starNr === 2).map(s => {
return {
x: s.getStarMoment,
y: s.nrOfPointsAfterThisOne,
star: s
}
})
};
});
let element = this.createGraphCanvas(data, "Points over time per member.");
this.graphs.appendChild(element);
let chart = new Chart(element.getContext("2d"), {
type: "line",
data: {
datasets: datasets,
},
options: {
responsive: true,
tooltips: {
callbacks: {
afterLabel: (item, data) => {
const star = data.datasets[item.datasetIndex].data[item.index].star;
return `(completed day ${star.dayNr} star ${star.starNr})`;
},
},
},
chartArea: {
backgroundColor: "rgba(0, 0, 0, 0.25)"
},
legend: {
position: "right",
labels: {
fontColor: aocColors["main"],
},
onClick: legendOnClick
},
title: {
display: true,
text: "Leaderboard (points)",
fontSize: 24,
fontStyle: "normal",
fontColor: aocColors["main"],
lineHeight: 2.0,
},
scales: {
xAxes: [{
type: "time",
time: {
unit: "day",
stepSize: 1,
displayFormats: {
day: "D"
},
},
ticks: {
min: moment([data.year, 10, 30, 5, 0, 0]),
max: data.maxMoment,
fontColor: aocColors["main"],
},
scaleLabel: {
display: true,
labelString: "Day of Advent",
fontColor: aocColors["main"],
},
gridLines: {
color: aocColors["tertiary"],
zeroLineColor: aocColors["secondary"],
},
}],
yAxes: [{
ticks: {
min: 0,
fontColor: aocColors["main"],
},
scaleLabel: {
display: true,
labelString: "cumulative points",
fontColor: aocColors["main"],
},
gridLines: {
color: aocColors["tertiary"],
zeroLineColor: aocColors["secondary"],
},
}],
}
}
});
return data;
}
loadStarsOverTime(data) {
let datasets = data.members.map(m => {
return {
label: m.name,
lineTension: 0.2,
fill: false,
borderWidth: 1.5,
borderColor: m.color,
backgroundColor: m.color,
data: m.stars.filter(s => s.starNr === 2).map(s => {
return {
x: s.getStarMoment,
y: s.nrOfStarsAfterThisOne,
star: s
};
}),
}
});
let element = this.createGraphCanvas(data, "Number of stars over time per member.");
this.graphs.appendChild(element);
let chart = new Chart(element.getContext("2d"), {
type: "line",
data: {
datasets: datasets,
},
options: {
responsive: true,
tooltips: {
callbacks: {
afterLabel: (item, data) => {
const star = data.datasets[item.datasetIndex].data[item.index].star;
return `(day ${star.dayNr} star ${star.starNr})`;
},
},
},
chartArea: {
backgroundColor: "rgba(0, 0, 0, 0.25)"
},
legend: {
position: "right",
labels: {
fontColor: aocColors["main"],
},
onClick: legendOnClick
},
title: {
display: true,
text: "Leaderboard (stars)",
fontSize: 24,
fontStyle: "normal",
fontColor: aocColors["main"],
lineHeight: 2.0,
},
scales: {
xAxes: [{
type: "time",
time: {
unit: "day",
stepSize: 1,
displayFormats: {
day: "D"
},
},
ticks: {
min: moment([data.year, 10, 30, 5, 0, 0]),
max: data.maxMoment,
fontColor: aocColors["main"],
},
scaleLabel: {
display: true,
labelString: "Day of Advent",
fontColor: aocColors["main"],
},
gridLines: {
color: aocColors["tertiary"],
zeroLineColor: aocColors["secondary"],
},
}],
yAxes: [{
ticks: {
stepSize: 1,
min: 0,
fontColor: aocColors["main"],
},
scaleLabel: {
display: true,
labelString: "nr of stars",
fontColor: aocColors["main"],
},
gridLines: {
color: aocColors["tertiary"],
zeroLineColor: aocColors["secondary"],
},
}],
}
}
});
return data;
}
}
aoc["App"] = App;
function loadAdditions() {
console.info("Going to construct App");
new aoc.App();
}
if (document.readyState === "complete" || document.readyState === "loaded" || document.readyState === "interactive") {
console.info(`Loading via readyState = ${document.readyState}`);
loadAdditions();
} else {
console.info(`Loading via DOMContentLoaded because readyState = ${document.readyState}`);
document.addEventListener("DOMContentLoaded", () => loadAdditions());
}
}(window.aoc = window.aoc || {}))
})
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment