├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE_NOTES.md ├── addon-assets ├── icon.pdn ├── icon128.png ├── icon16.png ├── icon48.png └── manifest.json ├── build.ps1 ├── build.sh ├── package-lock.json ├── package.json ├── readme-assets ├── browser-store-chrome.png ├── browser-store-firefox.png ├── example-001.png ├── example-002.png ├── example-003.png ├── example-004.png ├── example-005.png └── example-006.png └── src ├── css └── app.css ├── index.html └── js ├── app.js ├── dummyData-default.js ├── dummyData-empty.js ├── dummyData-large.js ├── dummyData-oneDay.js └── dummyData-regular.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to advent-of-code-charts 2 | 3 | This is only a tiny side-project, so let's keep it simple. 4 | Here's what you need to know: 5 | 6 | - First and foremost: be nice, open, welcoming, and inclusive to eachother! 7 | - This is a fun side-project made by an enthusiast. It's not endorsed by- or affiliated with (the creator of) Advent of Code (the name, the project, the puzzles: they're Eric Wastl's) - keep that in mind when contributing. 8 | - I accept PRs and welcome issue reports. For big changes open an issue first please, so we can discuss it before loads of efforts are put in. 9 | 10 | For info on how to build, run, and debug things: check out the readme. 11 | Or ping [me on Twitter](https://twitter.com/jeroenheijmans) 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017, Jeroen Heijmans 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Advent of Code Charts 2 | 3 | This is an [unofficial](#license-and-affiliation-disclaimer) small hacked-up set of charts for a private leaderboard for [Advent of Code](https://adventofcode.com/). 4 | Get it as an extension: 5 | 6 | [![browser-store-chrome.png](readme-assets/browser-store-chrome.png)](https://chrome.google.com/webstore/detail/advent-of-code-charts/ipbomkmbokofodhhjpipflmdplipblbe) [![browser-store-firefox.png](readme-assets/browser-store-firefox.png)](https://addons.mozilla.org/en-US/firefox/addon/advent-of-code-charts/) 7 | 8 | ## Disclaimers 9 | 10 | It is *not* a well-architectured, well-written, neat, nice, fluffy, industry-strength piece of code. 11 | Instead it's something fun I wanted to make, stepping out of my *normal* way of coding. NO WARRANTY! 12 | 13 | ## Developing 14 | 15 | Install dependencies: 16 | 17 | ```sh 18 | npm ci # or npm install 19 | ``` 20 | 21 | Serve a test website with the dummy data: 22 | 23 | ```sh 24 | npm run start # runs 'serve' and 'watch' in parallel 25 | ``` 26 | 27 | And open up the URL that's announced in the console. 28 | 29 | ## Building 30 | 31 | Run `build.ps1` or `build.sh` to re-create a `/build` folder which is a ready-to-go browser extension. 32 | Test the extension by loading it in the browser. 33 | For full reference, see Chrome's or Firefox's full documentation, but the basics are: 34 | 35 | - Firefox: go to `about:debugging` and load a temporary addon (pick the `/build` folder) 36 | - Chrome: go to `chrome://extensions` and load unpacked extension (pick the `/build` folder) 37 | 38 | Test by browsing to a private leaderboard and you should see charts popping up at the bottom. 39 | 40 | ## Releasing 41 | 42 | To release an addon to the store, for Chrome you just zip the `/build` folder files into a file and submit it as a new version. 43 | 44 | For Firefox, you need to add this to the manifest in the `/build` folder first: 45 | 46 | ```json 47 | "browser_specific_settings": { 48 | "gecko": { 49 | "id": "{GUID-GUID-GUID-GUID}" 50 | } 51 | } 52 | ``` 53 | 54 | For details see [Mozilla's documentation](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/browser_specific_settings) and [this Stack Overflow post](https://stackoverflow.com/q/56271601/419956) as to why it cannot be there by default. 55 | (I suppose this step can be automated away later on.) 56 | 57 | ## License and Affiliation Disclaimer 58 | 59 | The code in this project is MIT licensed, with the explicit exception of `dummyData.js`. 60 | That file contains JSON in a format thought up by the owner and creator of Advent of Code, but we suppose that using a small snippet of it like this falls under "fair use" (given for one that the AoC website itself suggests using the "JSON API" for integrations, albeit without spamming that API). 61 | 62 | Note that "Advent of Code" and "AoC" are Eric Wastl's. 63 | This project is not "official", and in no way (directly or indirectly) endorsed by- or affiliated to Advent of Code and its creator/owner. 64 | Read more [about Advent of Code](https://adventofcode.com/2018/about) to learn about the project itself. 65 | 66 | **Oh, and of course, please [consider donating to _Advent of Code_ itself](https://adventofcode.com/support)!** 67 | 68 | ## Example 69 | 70 | Here's what it should more or less look like: 71 | 72 | ![example-001.png](readme-assets/example-001.png) 73 | ![example-002.png](readme-assets/example-002.png) 74 | ![example-003.png](readme-assets/example-003.png) 75 | ![example-004.png](readme-assets/example-004.png) 76 | ![example-005.png](readme-assets/example-005.png) 77 | ![example-006.png](readme-assets/example-006.png) 78 | -------------------------------------------------------------------------------- /RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | Some adhoc release notes for changes made, in reverse chronological order. 4 | 5 | # 2024 AoC edition 6 | 7 | # 2024-12-07 8 | 9 | - Issue #105, Firefox error on first load fix 10 | 11 | # 2024-11-xx November fixes 12 | 13 | - Issue #54, upgrade to Manifest v3 was fixed 14 | 15 | ## 2023 AoC edition 16 | 17 | ### 2023-12-22 Mozilla FireFox update 18 | 19 | - Reviewers at Mozilla had requested we'd remove [the MomentJS workaround](https://github.com/jeroenheijmans/advent-of-code-charts/commit/6ea4669fe5588629e110db055313b9c772ba8331) we needed thus far, and it was possible since ChartJS had received updates in the past it seems. So we did a FireFox-only release to patch this 20 | 21 | ### 2023-12-01 Bugfix 22 | 23 | - Issue #95, bug with incorrect gold medal counts (thanks for reporting @diogotcorreia and submitting a PR to fix it @FXCourel) 24 | 25 | ### 2023-11-xx November fixes 26 | 27 | - Issue #87 was fixed, tooltips in 3rd graph now work properly again 28 | - Issue #91 was implemented, a "no data" message is displayed when there is no data (yet) 29 | - PR #94 implements issue #93: "Full Screen" mode, to showcase a leaderboard on a monitor/screen 30 | 31 | ### 2023-xx-xx Q1 fixes 32 | 33 | A bunch of fixes early in the year. 34 | 35 | List of fixes for #79, the 2023 updates: 36 | 37 | - PR #55 to add highlighting of yourself (the leaderboard owner) in various places 38 | - PR #78 to fix edge cases when the star timestamp is exactly the same for two users 39 | - Issue #56 and PR #61 to improve seeing Medals on older systems with less modern fonts 40 | - PR #62 to improve the final chart with time taken per stars 41 | - Issue #76 to show star times even if they are beyond 240 minutes (just show 'em clipped) 42 | - PR #73 and issue #4 to refactor the chart options and DRY them up a bit 43 | - PR #60 with new variants of the Points-Per-Day graph 44 | - Issue #81 update Chart.js and other dependencies 45 | - Issue #33 make (double) clicking legend items more discoverable 46 | - PR #58 to add more Delta-Time leaderboard features 47 | - See issue #54: rolled back to manifest v2 for now 48 | 49 | Further 2023 Q1 fixes and changes: 50 | 51 | - Issue #70 to improve situation for large and huge leaderboards 52 | 53 | ## 2022 and before 54 | 55 | There are no release notes from 2022 and before, except what can be found on Reddit and other various places. -------------------------------------------------------------------------------- /addon-assets/icon.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/advent-of-code-charts/71f0c9cbe75950e663cb2c0bfbf30eff20864fe8/addon-assets/icon.pdn -------------------------------------------------------------------------------- /addon-assets/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/advent-of-code-charts/71f0c9cbe75950e663cb2c0bfbf30eff20864fe8/addon-assets/icon128.png -------------------------------------------------------------------------------- /addon-assets/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/advent-of-code-charts/71f0c9cbe75950e663cb2c0bfbf30eff20864fe8/addon-assets/icon16.png -------------------------------------------------------------------------------- /addon-assets/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/advent-of-code-charts/71f0c9cbe75950e663cb2c0bfbf30eff20864fe8/addon-assets/icon48.png -------------------------------------------------------------------------------- /addon-assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Advent of Code Charts", 4 | "short_name": "AocCharts", 5 | "version": "7.0.1", 6 | "author": "Jeroen Heijmans", 7 | "description": "Inject charts in your private leaderboard page for Advent of Code - see https://github.com/jeroenheijmans/advent-of-code-charts", 8 | "icons": { 9 | "16": "icon16.png", 10 | "48": "icon48.png", 11 | "128": "icon128.png" 12 | }, 13 | "content_scripts": [ 14 | { 15 | "matches": ["http://adventofcode.com/*", "https://adventofcode.com/*"], 16 | "css": ["app.css"], 17 | "js": ["moment.min.js", "chart.umd.js", "chartjs-adapter-moment.min.js", "app.js"], 18 | "run_at": "document_end" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | Remove-Item build -Recurse -Force -Confirm:$false 2 | mkdir build 3 | 4 | Copy-Item addon-assets/icon*.png build/ 5 | Copy-Item node_modules/moment/min/moment.min.js build/ 6 | Copy-Item node_modules/chart.js/dist/chart.umd.js build/ 7 | Copy-Item node_modules/chartjs-adapter-moment/dist/chartjs-adapter-moment.min.js build/ 8 | Copy-Item src/js/app.js build/app.js 9 | Copy-Item src/css/app.css build/app.css 10 | Copy-Item addon-assets/manifest.json build/manifest.json 11 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | rm -rf build 3 | mkdir build 4 | 5 | cp addon-assets/icon*.png build/ 6 | cp node_modules/moment/min/moment.min.js build/moment.min.js 7 | cp node_modules/chart.js/dist/chart.umd.js build/chart.umd.js 8 | cp node_modules/chartjs-adapter-moment/dist/chartjs-adapter-moment.min.js build/chartjs-adapter-moment.min.js 9 | cp src/js/app.js build/app.js 10 | cp src/css/app.css build/app.css 11 | cp addon-assets/manifest.json build/manifest.json 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "advent-of-code-charts", 3 | "description": "Adhoc solution for Advent of Code leaderboard charts", 4 | "scripts": { 5 | "start": "npm-run-all --parallel serve watch", 6 | "serve": "serve src/.", 7 | "watch": "livereload src/." 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/jeroenheijmans/advent-of-code-charts.git" 12 | }, 13 | "author": "Jeroen Heijmans", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/jeroenheijmans/advent-of-code-charts/issues" 17 | }, 18 | "homepage": "https://github.com/jeroenheijmans/advent-of-code-charts#readme", 19 | "dependencies": { 20 | "chart.js": "4.4.6", 21 | "chartjs-adapter-moment": "1.0.1", 22 | "moment": "2.30.1" 23 | }, 24 | "devDependencies": { 25 | "livereload": "0.9.3", 26 | "npm-run-all": "4.1.5", 27 | "serve": "14.2.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /readme-assets/browser-store-chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/advent-of-code-charts/71f0c9cbe75950e663cb2c0bfbf30eff20864fe8/readme-assets/browser-store-chrome.png -------------------------------------------------------------------------------- /readme-assets/browser-store-firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/advent-of-code-charts/71f0c9cbe75950e663cb2c0bfbf30eff20864fe8/readme-assets/browser-store-firefox.png -------------------------------------------------------------------------------- /readme-assets/example-001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/advent-of-code-charts/71f0c9cbe75950e663cb2c0bfbf30eff20864fe8/readme-assets/example-001.png -------------------------------------------------------------------------------- /readme-assets/example-002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/advent-of-code-charts/71f0c9cbe75950e663cb2c0bfbf30eff20864fe8/readme-assets/example-002.png -------------------------------------------------------------------------------- /readme-assets/example-003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/advent-of-code-charts/71f0c9cbe75950e663cb2c0bfbf30eff20864fe8/readme-assets/example-003.png -------------------------------------------------------------------------------- /readme-assets/example-004.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/advent-of-code-charts/71f0c9cbe75950e663cb2c0bfbf30eff20864fe8/readme-assets/example-004.png -------------------------------------------------------------------------------- /readme-assets/example-005.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/advent-of-code-charts/71f0c9cbe75950e663cb2c0bfbf30eff20864fe8/readme-assets/example-005.png -------------------------------------------------------------------------------- /readme-assets/example-006.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jeroenheijmans/advent-of-code-charts/71f0c9cbe75950e663cb2c0bfbf30eff20864fe8/readme-assets/example-006.png -------------------------------------------------------------------------------- /src/css/app.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --aoc-colors-main: rgba(200, 200, 200, 0.9); 3 | --aoc-colors-secondary: rgba(150, 150, 150, 0.9); 4 | --aoc-colors-tertiary: rgba(100, 100, 100, 0.5); 5 | --aoc-colors-highlight: rgba(119,119,165,.2); 6 | --aoc-colors-link: #009900; 7 | } 8 | 9 | body.aoc-extension-with-full-screen-overlay > * { 10 | display: none !important; 11 | } 12 | 13 | body.aoc-extension-with-full-screen-overlay .aoc-extension-full-screen-warning { 14 | display: block !important; 15 | position: absolute; 16 | top: 5px; 17 | left: 50%; 18 | transform: translate(-50%, 0); 19 | text-align: center; 20 | width: 95%; 21 | box-sizing: border-box; 22 | color: var(--aoc-colors-tertiary); 23 | } 24 | 25 | body.aoc-extension-with-full-screen-overlay #aoc-extension { 26 | display: block !important; 27 | text-align: center; 28 | } 29 | 30 | body.aoc-extension-with-full-screen-overlay #aoc-extension > * { 31 | display: none !important; 32 | } 33 | 34 | body.aoc-extension-with-full-screen-overlay #aoc-extension > .aoc-extension-full-screen-subject { 35 | display: inline-block !important; 36 | margin: 50px auto; 37 | } 38 | 39 | body.aoc-extension-with-full-screen-overlay #aoc-extension > .aoc-extension-full-screen-subject#aoc-extension-graphs { 40 | display: grid !important; 41 | justify-items: center; 42 | max-width: 95%; 43 | max-height: 95%; 44 | } 45 | 46 | body.aoc-extension-with-full-screen-overlay #aoc-extension > .aoc-extension-full-screen-subject#aoc-extension-perDayLeaderBoard > table { 47 | width: 100%; 48 | } 49 | 50 | .aoc-extension-full-screen-exit-button { 51 | display: none; 52 | } 53 | 54 | body.aoc-extension-with-full-screen-overlay > .aoc-extension-full-screen-exit-button { 55 | display: inline-block !important; 56 | position: absolute; 57 | top: 10px; 58 | right: 10px; 59 | background: rgba(50, 50, 50, 0.8); 60 | padding: 10px; 61 | width: 20px; 62 | height: 20px; 63 | border: 1px solid rgba(75, 75, 75, 0.8); 64 | z-index: 101; 65 | display: flex; 66 | text-align: center; 67 | justify-content: center; 68 | cursor: pointer; 69 | } 70 | 71 | body.aoc-extension-with-full-screen-overlay > .aoc-extension-full-screen-exit-button:hover { 72 | background: rgba(75, 75, 75, 0.9); 73 | } 74 | 75 | .aoc-extension-full-screen-buttons { 76 | margin-left: 15px; 77 | display: inline-block; 78 | } 79 | 80 | .aoc-extension-full-screen-button { 81 | background: var(--aoc-colors-tertiary); 82 | border: 1px solid var(--aoc-colors-secondary); 83 | cursor: pointer; 84 | padding: 2px 4px; 85 | margin-left: 8px; 86 | } 87 | 88 | ::-webkit-scrollbar { 89 | width: 10px; 90 | } 91 | 92 | ::-webkit-scrollbar-track { 93 | background: #18181b; 94 | } 95 | 96 | ::-webkit-scrollbar-thumb { 97 | background: #52525b; 98 | } 99 | 100 | ::-webkit-scrollbar-thumb:hover { 101 | background: #3f3f46; 102 | } 103 | 104 | /* Firefox: */ 105 | * { 106 | scrollbar-width: thin; 107 | scrollbar-color: #52525b #18181b; 108 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Advent of Code Charts 5 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
Jeroen Heijmans (AoC++) 14*
30 |
31 |
32 | Regular AoC website content comes first. 33 |
34 | This is just a placeholder for that content. 35 |

36 | Choose testing with leaderboard: 37 |
38 | [Default] 39 | [Empty] 40 | [One Day] 41 | [Regular] 42 | [Large] 43 |

44 | The plugin content always sits below 45 |
46 | 47 | 48 | 49 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** 4 | * @typedef moment 5 | * @property {import('moment')} moment 6 | */ 7 | 8 | (function (/** @type {any} */ aoc) { 9 | // Unsure how to add JSDoc types so for now like this. 10 | // See also: https://stackoverflow.com/q/77466760/419956 11 | const Chart = /** @type {any} */ (window["Chart"]); 12 | 13 | // See https://stackoverflow.com/a/71395413/419956 by user @EJAntonPotot 14 | Chart.register({ 15 | id: 'custom_canvas_background_color', 16 | beforeDraw: (chart, _args, _options) => { 17 | const { 18 | ctx, 19 | chartArea: { top, left, width, height }, 20 | } = chart; 21 | ctx.save(); 22 | ctx.globalCompositeOperation = 'destination-over'; 23 | ctx.fillStyle = 'rgba(0, 0, 0, 0.25)'; 24 | ctx.fillRect(left, top, width, height); 25 | ctx.restore(); 26 | }, 27 | }); 28 | 29 | const aocColors = { 30 | "main": "rgba(200, 200, 200, 0.9)", 31 | "secondary": "rgba(150, 150, 150, 0.9)", 32 | "tertiary": "rgba(100, 100, 100, 0.5)", 33 | "highlight": "rgba(119,119,165,.2)", 34 | "link": "#009900", 35 | }; 36 | 37 | const graphColorStyles = [ 38 | "Original (1)", 39 | "Rainbow Alphabetic (2)", 40 | "Rainbow Score (3)", 41 | "Fire Alphabetic (4)", 42 | "Fire Score (5)" 43 | ]; 44 | 45 | const podiumLength = 3; 46 | const largeLeaderboardCutOff = Math.trunc((window.innerWidth < 1600 ? 25 : 40) / (isResponsivenessToggled() ? 2 : 1)); 47 | 48 | const pointsOverTimeType = [ 49 | "(◉ POINTS | ◎ percentage | ◎ with potential)", 50 | "(◎ points | ◉ PERCENTAGE | ◎ with potential)", 51 | "(◎ points | ◎ percentage | ◉ WITH POTENTIAL)", 52 | ]; 53 | 54 | let presumedLoggedInUserName = null; 55 | try { 56 | presumedLoggedInUserName = document.querySelector(".user")?.childNodes[0].textContent?.trim(); 57 | if (!presumedLoggedInUserName) { 58 | presumedLoggedInUserName = document 59 | .querySelector(".user") 60 | ?.textContent 61 | ?.replace("(AoC++) ", "") // Individual sponsor marking 62 | .replace("(Sponsor) ", "") // Company sponsor marking 63 | .replace(/\d\d?\*/, "") // E.g. "1*" or "50*" 64 | .trim(); 65 | } 66 | } catch (e) { 67 | console.info("Could not reliably determine logged in user from AoC website html. Something may have changed in the HTML structure, or perhaps there's an edge case we've missed. Either way, we'll ignore this error and carry on."); 68 | } 69 | 70 | /** 71 | * Create array of numbers in a range 72 | * @param {number} from - Starting number (inclusive) 73 | * @param {number} to - End of range (exclusive) 74 | * @returns {number[]} 75 | */ 76 | function range(from, to) { 77 | return [...Array(to - from).keys()].map(k => k + from); 78 | } 79 | 80 | /** 81 | * Compare two star objects based on the index (first or second star) 82 | * @param {{starIndex: number}} a Some star to compare 83 | * @param {{starIndex: number}} b Some star to compare 84 | * @returns {number} 85 | */ 86 | function starSorter(a, b) { 87 | return a.starIndex - b.starIndex; 88 | } 89 | 90 | /** 91 | * Compare two entries by deltaPointsTotal 92 | * @param {{deltaPointsTotal: number}} a Some day to compare 93 | * @param {{deltaPointsTotal: number}} b Some day to compare 94 | * @returns {number} 95 | */ 96 | function deltaPointsTotalSorter(a, b) { 97 | return a.deltaPointsTotal - b.deltaPointsTotal; 98 | } 99 | 100 | /** 101 | * Get an array of color strings for a fixed number of entries 102 | * @param {number} n Number of distinct colors needed 103 | * @param {boolean} rainbow Whether to create a rainbow pattern 104 | * @param {boolean} original Whether to use the original color pattern 105 | * @returns {string[]} 106 | */ 107 | function getPalette(n, rainbow, original) { 108 | if (original) { 109 | const basePalette = ["rgba(120, 28, 129, 1.0)", "rgba(110, 25, 128, 1.0)", "rgba(101, 24, 127, 1.0)", "rgba(94, 24, 126, 1.0)", "rgba(88, 25, 126, 1.0)", "rgba(83, 27, 127, 1.0)", "rgba(79, 29, 129, 1.0)", "rgba(76, 33, 130, 1.0)", "rgba(73, 36, 132, 1.0)", "rgba(70, 41, 135, 1.0)", "rgba(68, 45, 138, 1.0)", "rgba(67, 50, 141, 1.0)", "rgba(66, 55, 145, 1.0)", "rgba(65, 61, 148, 1.0)", "rgba(64, 66, 152, 1.0)", "rgba(63, 72, 156, 1.0)", "rgba(63, 78, 160, 1.0)", "rgba(63, 83, 165, 1.0)", "rgba(63, 89, 169, 1.0)", "rgba(63, 95, 173, 1.0)", "rgba(64, 100, 177, 1.0)", "rgba(64, 105, 181, 1.0)", "rgba(65, 111, 184, 1.0)", "rgba(66, 116, 187, 1.0)", "rgba(67, 121, 190, 1.0)", "rgba(68, 125, 192, 1.0)", "rgba(69, 130, 193, 1.0)", "rgba(70, 134, 194, 1.0)", "rgba(72, 138, 194, 1.0)", "rgba(74, 142, 193, 1.0)", "rgba(75, 146, 192, 1.0)", "rgba(77, 149, 190, 1.0)", "rgba(79, 153, 187, 1.0)", "rgba(81, 156, 184, 1.0)", "rgba(84, 159, 180, 1.0)", "rgba(86, 162, 176, 1.0)", "rgba(88, 164, 172, 1.0)", "rgba(91, 167, 167, 1.0)", "rgba(94, 169, 162, 1.0)", "rgba(96, 171, 157, 1.0)", "rgba(99, 173, 152, 1.0)", "rgba(102, 175, 147, 1.0)", "rgba(105, 177, 142, 1.0)", "rgba(108, 178, 137, 1.0)", "rgba(112, 180, 132, 1.0)", "rgba(115, 181, 128, 1.0)", "rgba(119, 182, 123, 1.0)", "rgba(122, 184, 119, 1.0)", "rgba(126, 185, 115, 1.0)", "rgba(130, 186, 111, 1.0)", "rgba(133, 186, 107, 1.0)", "rgba(137, 187, 104, 1.0)", "rgba(141, 188, 101, 1.0)", "rgba(145, 189, 97, 1.0)", "rgba(149, 189, 94, 1.0)", "rgba(153, 189, 92, 1.0)", "rgba(157, 190, 89, 1.0)", "rgba(161, 190, 86, 1.0)", "rgba(165, 190, 84, 1.0)", "rgba(169, 190, 82, 1.0)", "rgba(173, 190, 80, 1.0)", "rgba(177, 190, 78, 1.0)", "rgba(181, 189, 76, 1.0)", "rgba(185, 189, 74, 1.0)", "rgba(188, 188, 72, 1.0)", "rgba(192, 187, 71, 1.0)", "rgba(195, 186, 69, 1.0)", "rgba(199, 185, 68, 1.0)", "rgba(202, 184, 67, 1.0)", "rgba(205, 182, 65, 1.0)", "rgba(208, 181, 64, 1.0)", "rgba(211, 179, 63, 1.0)", "rgba(214, 177, 62, 1.0)", "rgba(216, 174, 61, 1.0)", "rgba(219, 171, 60, 1.0)", "rgba(221, 169, 59, 1.0)", "rgba(223, 165, 58, 1.0)", "rgba(224, 162, 57, 1.0)", "rgba(226, 158, 56, 1.0)", "rgba(227, 154, 55, 1.0)", "rgba(228, 150, 54, 1.0)", "rgba(229, 146, 53, 1.0)", "rgba(230, 141, 52, 1.0)", "rgba(231, 136, 51, 1.0)", "rgba(231, 131, 50, 1.0)", "rgba(231, 125, 49, 1.0)", "rgba(231, 119, 48, 1.0)", "rgba(231, 113, 47, 1.0)", "rgba(230, 107, 45, 1.0)", "rgba(230, 100, 44, 1.0)", "rgba(229, 94, 43, 1.0)", "rgba(228, 87, 42, 1.0)", "rgba(227, 80, 41, 1.0)", "rgba(226, 73, 40, 1.0)", "rgba(225, 66, 38, 1.0)", "rgba(223, 59, 37, 1.0)", "rgba(222, 52, 36, 1.0)", "rgba(220, 46, 34, 1.0)", "rgba(219, 39, 33, 1.0)", "rgba(217, 33, 32, 1.0)"]; 110 | let step = basePalette.length / n; 111 | return [...Array(n).keys()].map(i => basePalette[Math.floor(i * step)]); 112 | } 113 | 114 | if (rainbow) 115 | // Dynamic rainbow palette using hsl() 116 | return [...Array(n).keys()].map(i => "hsla(" + i * 300 / n + ", 100%, 50%, 1.0)"); 117 | 118 | // Dynamic fire palette red->yellow->green using hsl() 119 | return [...Array(n).keys()].map(i => "hsla(" + i * 120 / n + ", 100%, 50%, 1.0)") 120 | } 121 | 122 | /** 123 | * Take a color string that already has an "1.0" alpha component, and do a quick-and-dirty replace with a new alpha value 124 | * @param {string} color 125 | * @param {string|number} newAlpha 126 | * @returns {string} 127 | */ 128 | function lowerAlpha(color, newAlpha) { 129 | // Look, this extensions is in "AoC-style", so we're allowed 130 | // some shortcuts that cannot be done in software that is meant 131 | // to earn big bugs or save lives or whatever. :-) 132 | return color.replace(", 1.0)", `, ${newAlpha})`) 133 | } 134 | 135 | /** 136 | * Adjust the points for a star of a given day (used to compensate for exceptions, e.g. a day that was worth 0 points) 137 | * @param {number} year 138 | * @param {string} dayKey 139 | * @param {string} _starKey 140 | * @param {number} basePoints 141 | * @returns {number} 142 | */ 143 | function adjustPoinstFor(year, dayKey, _starKey, basePoints) { 144 | // https://github.com/jeroenheijmans/advent-of-code-charts/issues/18 145 | if (year === 2018 && dayKey === "6") { 146 | return 0; 147 | } 148 | 149 | if (year === 2020 && dayKey === "1") { 150 | return 0; 151 | } 152 | 153 | return basePoints; 154 | } 155 | 156 | /** 157 | * @typedef {{ 158 | * last_star_ts: string | number; 159 | * global_score: number; 160 | * completion_day_level: Record>; 161 | * local_score: number; 162 | * name: string | null; 163 | * stars: number; 164 | * id: string; 165 | * }} IMemberJson 166 | * 167 | * @typedef {{ 168 | * event: string, 169 | * owner_id: string, 170 | * members: Record 171 | * }} IJson 172 | * 173 | * @typedef {{ 174 | * memberId: string; 175 | * dayNr: number; 176 | * dayKey: string; 177 | * starIndex: number; 178 | * starNr: number; 179 | * starKey: string; 180 | * getStarDay: number; 181 | * getStarTimestamp: number; 182 | * getStarMoment: moment.Moment; 183 | * timeTaken: number; 184 | * timeTakenSeconds: number; 185 | * nrOfStarsAfterThisOne: number; 186 | * points: number; 187 | * rank: number; 188 | * nrOfPointsAfterThisOne: number; 189 | * awardedPodiumPlace: number; 190 | * awardedPodiumPlaceFirstPuzzle: number; 191 | * }} IStar 192 | * 193 | * @typedef {Omit & { 194 | * isLoggedInUser: boolean; 195 | * radius: number; 196 | * borderWidth: number; 197 | * pointStyle: string; 198 | * stars: IStar[]; 199 | * deltas: IDelta[]; 200 | * deltaPointsTotal: number; 201 | * deltaMeanSeconds: number; 202 | * deltaMedianSeconds: number; 203 | * rank: number; 204 | * score: number; 205 | * podiumPlacesPerDay: number[]; 206 | * podiumPlacesPerDayFirstPuzzle: number[]; 207 | * color: string; 208 | * colorMuted: string; 209 | * }} IMember 210 | * 211 | * @typedef {{ 212 | * dayNr: number; 213 | * dayKey: string; 214 | * deltaTimeTakenSeconds: number; 215 | * member: IMember; 216 | * points: number; 217 | * }} IDelta 218 | * 219 | * @typedef {ReturnType} IData; 220 | * 221 | * @typedef {{ 222 | * owner_id: string, 223 | * maxDay: number, 224 | * maxMoment: moment.Moment, 225 | * days: IDaysMap, 226 | * stars: IStar[], 227 | * members: IMember[], 228 | * year: number, 229 | * n_members: number, 230 | * maxDeltaPoints, 231 | * loggedInUserIsPresumablyKnown, 232 | * isLargeLeaderboard, 233 | * }} IAppData 234 | * 235 | * @typedef {Record} IDaysMap 240 | */ 241 | 242 | /** 243 | * @param {IJson} json 244 | * @returns {IAppData} 245 | */ 246 | function transformRawAocJson(json) { 247 | let /** @type {IStar[]} */ stars = []; 248 | let /** @type {IDelta[]} */ deltas = []; 249 | let year = parseInt(json.event); 250 | let loggedInUserIsPresumablyKnown = false; 251 | 252 | let n_members = Object.keys(json.members).length; 253 | let isLargeLeaderboard = n_members > largeLeaderboardCutOff; 254 | 255 | let members = Object.keys(json.members) 256 | .map(k => json.members[k]) 257 | .map(m => /** @type {IMember} */ (/** @type {unknown} */ (m))) 258 | .map((m) => { 259 | m.isLoggedInUser = m.name === presumedLoggedInUserName; 260 | loggedInUserIsPresumablyKnown = loggedInUserIsPresumablyKnown || m.isLoggedInUser; 261 | 262 | m.radius = m.isLoggedInUser ? 4 : 3; 263 | m.borderWidth = m.isLoggedInUser ? 2.5 : 1; 264 | m.pointStyle = m.isLoggedInUser ? "rectRot" : "circle" 265 | m.stars = []; 266 | m.deltas = []; 267 | m.deltaPointsTotal = 0; // Calculated later 268 | m.deltaMeanSeconds = 0; // Calculated later 269 | m.deltaMedianSeconds = 0; // Calculated later 270 | m.name = m.name || `(anonymous user ${m.id})`; 271 | 272 | for (let dayKey of Object.keys(m.completion_day_level)) { 273 | for (let starKey of Object.keys(m.completion_day_level[dayKey])) { 274 | let starMoment = moment.unix(+m.completion_day_level[dayKey][starKey].get_star_ts).utc(); 275 | 276 | let /** @type {IStar} */ star = { 277 | memberId: m.id, 278 | dayNr: parseInt(dayKey, 10), 279 | dayKey: dayKey, 280 | starNr: parseInt(starKey, 10), 281 | starKey: starKey, 282 | getStarDay: parseInt(`${dayKey}.${starKey}`, 10), 283 | getStarTimestamp: m.completion_day_level[dayKey][starKey].get_star_ts, 284 | getStarMoment: starMoment, 285 | starIndex: m.completion_day_level[dayKey][starKey].star_index, 286 | 287 | // Setting defaults, calculating these properties later on in loops 288 | timeTaken: 0, 289 | timeTakenSeconds: 0, 290 | nrOfStarsAfterThisOne: 0, 291 | nrOfPointsAfterThisOne: 0, 292 | points: 0, 293 | rank: 0, 294 | awardedPodiumPlace: -1, 295 | awardedPodiumPlaceFirstPuzzle: -1, 296 | }; 297 | 298 | stars.push(star); 299 | m.stars.push(star); 300 | } 301 | } 302 | 303 | m.stars = m.stars.sort(starSorter); 304 | 305 | m.stars.forEach((s, idx) => { 306 | s.nrOfStarsAfterThisOne = idx + 1; 307 | 308 | let startOfDay = moment.utc([year, 11, s.dayNr, 5, 0, 0]); // AoC starts at 05:00 UTC 309 | s.timeTaken = s.getStarMoment.diff(startOfDay, "minutes"); 310 | s.timeTakenSeconds = s.getStarMoment.diff(startOfDay, "seconds"); 311 | }); 312 | 313 | m.stars.filter(s => s.starNr === 2).forEach(star2 => { 314 | const star1 = m.stars.find(s => s.dayNr === star2.dayNr && s.starNr === 1); 315 | const deltaTimeTakenSeconds = star2.timeTakenSeconds - (star1?.timeTakenSeconds || 0); 316 | const delta = { dayNr: star2.dayNr, dayKey: star2.dayKey, deltaTimeTakenSeconds, member: m, points: 0, }; 317 | deltas.push(delta); 318 | m.deltas.push(delta); 319 | }); 320 | 321 | const sortedDeltas = m.deltas 322 | .slice() 323 | .filter(d => d.deltaTimeTakenSeconds < 4 * 60 * 60) // Outliers distort images so we exclude them 324 | .sort((a,b) => a.deltaTimeTakenSeconds - b.deltaTimeTakenSeconds); 325 | if (sortedDeltas.length > 0) { 326 | m.deltaMedianSeconds = sortedDeltas[Math.trunc(sortedDeltas.length / 2)].deltaTimeTakenSeconds; 327 | m.deltaMeanSeconds = sortedDeltas.map(x => x.deltaTimeTakenSeconds).reduce((a,b) => a+b) / sortedDeltas.length; 328 | } 329 | 330 | return m; 331 | }) 332 | .filter(m => m.stars.length > 0) 333 | .sort((a, b) => a.name?.localeCompare(b?.name || "") || 0); 334 | 335 | let allMoments = stars.map(s => s.getStarMoment).concat([moment("" + year + "-12-25T00:00:00-0000")]); 336 | let maxMoment = moment.min([moment.max(allMoments), moment("" + year + "-12-31T00:00:00-0000")]); 337 | 338 | const maxDeltaPoints = members.filter(m => m.deltas.length > 0).length; 339 | for (let i = 1; i <= 25; i++) { 340 | let availableDeltaPoints = maxDeltaPoints; 341 | const sortedDeltas = deltas.filter(d => d.dayNr === i).sort((a,b) => a.deltaTimeTakenSeconds - b.deltaTimeTakenSeconds); 342 | for (let delta of sortedDeltas) { 343 | delta.points = availableDeltaPoints--; 344 | delta.member.deltaPointsTotal += delta.points; 345 | } 346 | } 347 | 348 | let availablePoints = {}; 349 | 350 | for (let i = 1; i <= 25; i++) { 351 | availablePoints[i] = {}; 352 | for (let j = 1; j <= 2; j++) { 353 | availablePoints[i][j] = n_members; 354 | } 355 | } 356 | 357 | let orderedStars = stars.sort(starSorter); 358 | 359 | for (let star of orderedStars) { 360 | const basePoints = availablePoints[star.dayKey][star.starKey]--; 361 | star.points = adjustPoinstFor(year, star.dayKey, star.starKey, basePoints); 362 | star.rank = n_members - basePoints + 1; 363 | } 364 | 365 | for (let m of members) { 366 | let accumulatedPoints = 0; 367 | for (let s of m.stars.sort(starSorter)) { 368 | accumulatedPoints += s.points; 369 | s.nrOfPointsAfterThisOne = accumulatedPoints; 370 | m.score = accumulatedPoints; 371 | } 372 | } 373 | 374 | /** @type {number} */ 375 | let maxDay = Math.max.apply(Math, stars.filter(s => s.starNr === 2).map(s => s.dayNr)) 376 | 377 | /** @type {IDaysMap} */ 378 | let days = {}; 379 | 380 | for (let d = 1; d <= maxDay; d++) { 381 | days[d] = { 382 | dayNr: d, 383 | podium: stars.filter(s => s.dayNr === d && s.starNr === 2).sort(starSorter), 384 | podiumFirstPuzzle: stars.filter(s => s.dayNr === d && s.starNr === 1).sort(starSorter), 385 | }; 386 | 387 | for (let i = 0; i < days[d].podium.length; i++) { 388 | days[d].podium[i].awardedPodiumPlace = i; 389 | days[d].podiumFirstPuzzle[i].awardedPodiumPlaceFirstPuzzle = i; 390 | } 391 | } 392 | 393 | for (let m of members) { 394 | m.podiumPlacesPerDay = getPodiumFor(m); 395 | m.podiumPlacesPerDayFirstPuzzle = getPodiumForFirstPuzzle(m); 396 | } 397 | 398 | let curGraphColorStyle = (getCurrentGraphColorStyle() || "").toLowerCase(); 399 | let isOriginal = curGraphColorStyle.includes("original"); 400 | let isRainbow = curGraphColorStyle.includes("rainbow"); 401 | let orderByScore = curGraphColorStyle.includes("score"); 402 | let colors = getPalette(members.length, isRainbow, isOriginal); 403 | let muteFactor = 0.25 + ((200 - n_members) / 200 * 0.5); 404 | 405 | members 406 | .slice() 407 | .sort((a, b) => b.score - a.score) 408 | .forEach((m, idx) => { m.rank = idx + 1; }); 409 | 410 | if (orderByScore) 411 | members 412 | .sort((a, b) => b.score - a.score) 413 | .forEach((m, idx) => { 414 | const color = colors[idx]; 415 | m.color = color; 416 | m.colorMuted = lowerAlpha(colors[idx], muteFactor); 417 | m.rank = idx + 1; 418 | }); 419 | else 420 | members 421 | .forEach((m, idx) => { 422 | const color = colors[idx]; 423 | m.color = color; 424 | m.colorMuted = lowerAlpha(colors[idx], muteFactor); 425 | }); 426 | 427 | return { 428 | owner_id: json.owner_id, 429 | maxDay: maxDay, 430 | maxMoment: maxMoment, 431 | days: days, 432 | stars: stars, 433 | members: members, 434 | year: year, 435 | n_members: n_members, 436 | maxDeltaPoints, 437 | loggedInUserIsPresumablyKnown, 438 | isLargeLeaderboard, 439 | }; 440 | } 441 | 442 | function getPodiumFor(/** @type IMember */ member) { 443 | let medals = []; 444 | for (let p = 0; p < podiumLength; p++) { 445 | medals.push(member.stars.filter(s => s.awardedPodiumPlace === p).length); 446 | } 447 | return medals; 448 | } 449 | 450 | function getPodiumForFirstPuzzle(/** @type IMember */ member) { 451 | let medals = []; 452 | for (let p = 0; p < podiumLength; p++) { 453 | medals.push(member.stars.filter(s => s.awardedPodiumPlaceFirstPuzzle === p).length); 454 | } 455 | return medals; 456 | } 457 | 458 | function memberByPodiumSorter(/** @type IMember */ a, /** @type IMember */ b) { 459 | let aMedals = getPodiumFor(a); 460 | let bMedals = getPodiumFor(b); 461 | 462 | for (let i = 0; i < aMedals.length; i++) { 463 | if (aMedals[i] !== bMedals[i]) { 464 | return bMedals[i] - aMedals[i]; 465 | } 466 | } 467 | 468 | aMedals = getPodiumForFirstPuzzle(a); 469 | bMedals = getPodiumForFirstPuzzle(b); 470 | 471 | for (let i = 0; i < aMedals.length; i++) { 472 | if (aMedals[i] !== bMedals[i]) { 473 | return bMedals[i] - aMedals[i]; 474 | } 475 | } 476 | 477 | return b.local_score - a.local_score; 478 | } 479 | 480 | function getCacheKey() { 481 | return `aoc-data-v1-${document.location.pathname}`; 482 | } 483 | 484 | function getCache() { 485 | console.info("Getting cache", getCacheKey()); 486 | return JSON.parse(localStorage.getItem(getCacheKey()) || "null"); 487 | } 488 | 489 | function updateCache(data) { 490 | console.log("Updating cache"); 491 | localStorage.setItem(getCacheKey(), JSON.stringify({ data: data, timestamp: Date.now() })); 492 | return data; 493 | } 494 | 495 | function clearCache() { 496 | console.log("Clearing cache", getCacheKey()); 497 | localStorage.setItem(getCacheKey(), ""); 498 | } 499 | 500 | function toggleShowAll() { 501 | localStorage.setItem("aoc-flag-v1-show-all", !isShowAllToggled() + ""); 502 | location.reload(); 503 | } 504 | 505 | function isShowAllToggled() { 506 | return !!JSON.parse(localStorage.getItem("aoc-flag-v1-show-all") || "null"); 507 | } 508 | 509 | function toggleResponsiveness() { 510 | localStorage.setItem("aoc-flag-v1-is-responsive", !isResponsivenessToggled() + ""); 511 | location.reload(); 512 | } 513 | 514 | function isResponsivenessToggled() { 515 | return !!JSON.parse(localStorage.getItem("aoc-flag-v1-is-responsive") || "null"); 516 | } 517 | 518 | function getCurrentGraphColorStyle() { 519 | return localStorage.getItem("aoc-flag-v1-color-style") || ""; 520 | } 521 | 522 | function toggleCurrentGraphColorStyle({ shouldReload = true } = {}) { 523 | let cur = graphColorStyles.indexOf(getCurrentGraphColorStyle()); 524 | localStorage.setItem("aoc-flag-v1-color-style", graphColorStyles[(cur + 1) % graphColorStyles.length]); 525 | if (shouldReload) location.reload(); 526 | } 527 | 528 | function setDisplayDay(/** @type string */ dayNumber) { 529 | localStorage.setItem("aoc-flag-v1-display-day", dayNumber); 530 | } 531 | 532 | function getDisplayDay() { 533 | return localStorage.getItem("aoc-flag-v1-display-day"); 534 | } 535 | 536 | function setTimeTableSort(/** @type string */ sort) { 537 | localStorage.setItem("aoc-flag-v1-delta-sort", sort); 538 | location.reload(); 539 | } 540 | 541 | function getTimeTableSort() { 542 | return localStorage.getItem("aoc-flag-v1-delta-sort") || "delta"; 543 | } 544 | 545 | function togglePointsOverTimeType() { 546 | const value = (getPointsOverTimeType() + 1) % pointsOverTimeType.length; 547 | localStorage.setItem("aoc-flag-v1-points-over-time-type-index", value.toString()); 548 | location.reload(); 549 | } 550 | 551 | function getPointsOverTimeType() { 552 | return +(localStorage.getItem("aoc-flag-v1-points-over-time-type-index") || "0") || 0; 553 | } 554 | 555 | /** @typedef {"medals"|"perDayLeaderBoard"|"graphs"|null} IFullScreenSubject */ 556 | function setFullScreenSubject(/** @type IFullScreenSubject */ subject) { 557 | localStorage.setItem("aoc-flag-v1-full-screen-subject", subject || ""); 558 | 559 | if (subject === "graphs" && !isResponsivenessToggled()) { 560 | toggleResponsiveness(); 561 | } 562 | } 563 | 564 | /** @returns {IFullScreenSubject} */ 565 | function getFullScreenSubject() { 566 | return /** @type IFullScreenSubject */ (localStorage.getItem("aoc-flag-v1-full-screen-subject")) || null; 567 | } 568 | 569 | const defaultLegendClickHandler = Chart.defaults.plugins.legend.onClick; 570 | let prevClick; 571 | function isDoubleClick() { 572 | let now = new Date(); 573 | if (!prevClick) { 574 | prevClick = now; 575 | return false; 576 | } 577 | 578 | let diff = now.getTime() - prevClick; 579 | prevClick = now; 580 | 581 | return diff < 300; 582 | } 583 | 584 | function formatTimeTaken(/** @type number */ seconds) { 585 | if (seconds > 24 * 3600) { 586 | return ">24h" 587 | } 588 | return moment().startOf('day').seconds(seconds).format('HH:mm:ss') 589 | } 590 | 591 | function formatStarMomentForTitle(/** @type IStar */ memberStar) { 592 | return memberStar.getStarMoment.local().format("HH:mm:ss YYYY-MM-DD") + " (local time)"; 593 | } 594 | 595 | /** 596 | * @returns {Promise} 597 | */ 598 | function getLeaderboardJson() { 599 | // 1. Check if dummy data was loaded... 600 | if (!!aoc.dummyData) { 601 | console.info("Loading dummyData"); 602 | 603 | return new Promise((resolve, reject) => { 604 | setTimeout(() => { 605 | resolve(transformRawAocJson(aoc.dummyData)); 606 | }, 10); 607 | }); 608 | } 609 | // 2. Apparently we can use real calls... 610 | else { 611 | let anchor = /** @type {HTMLAnchorElement} */ (document.querySelector("#api_info a")); 612 | 613 | if (!!anchor) { 614 | let url = anchor.href; 615 | 616 | const cache = getCache(); 617 | 618 | console.info("Found cache", cache); 619 | 620 | if (cache) { 621 | const ttl = new Date(cache.timestamp + (5 * 60 * 1000)); 622 | console.info("Found cached value valid until", ttl); 623 | 624 | if (Date.now() < ttl.getTime()) { 625 | console.info("Cache was still valid!"); 626 | 627 | return Promise.resolve(cache.data) 628 | .then(json => transformRawAocJson(json)); 629 | } 630 | } 631 | 632 | console.info(`Loading data from url ${url}`); 633 | 634 | return fetch(url, { credentials: "same-origin" }) 635 | .then(data => data.json()) 636 | .then(json => updateCache(json)) 637 | .then(() => getCache().data) // Workaround for FireFox error with "Xray Vision" / "XrayWrapper", see https://github.com/jeroenheijmans/advent-of-code-charts/issues/105 638 | .then(json => transformRawAocJson(json)); 639 | } else { 640 | console.info("Could not find anchor to JSON feed, assuming no charts can be plotted here."); 641 | return new Promise((resolve, reject) => { }); 642 | } 643 | } 644 | } 645 | 646 | class ChartOptions { 647 | constructor(data, /** @type string */ titleText) { 648 | this.responsive = true; 649 | this.aspectRation = 1; 650 | this.plugins = { 651 | legend: { 652 | position: "right", 653 | title: { 654 | display: true, 655 | // We compromise: for large leaderboards we really need to explain 656 | // that only the top N are given a legend item. For smaller 657 | // leaderboards (where all are shown) we make the click feature 658 | // discoverable. 659 | text: data.isLargeLeaderboard ? `Only Showing Top ${largeLeaderboardCutOff}` : "(🖱 click / 🖱🖱 click)", 660 | color: aocColors["main"], 661 | font: { weight: "bold", }, 662 | }, 663 | labels: { 664 | color: aocColors["main"], 665 | usePointStyle: true, 666 | }, 667 | onClick: function (event, legendItem, legend) { 668 | defaultLegendClickHandler(event, legendItem, legend); 669 | 670 | if (isDoubleClick()) { 671 | let chart = this.chart; 672 | 673 | // always show doubleclicked item 674 | chart.getDatasetMeta(legendItem.datasetIndex).hidden = null; 675 | 676 | // count how many hidden datasets are there 677 | let hiddenCount = chart.data.datasets 678 | .map((_, dataSetIndex) => chart.getDatasetMeta(dataSetIndex)) 679 | .filter(meta => meta.hidden) 680 | .length; 681 | 682 | // deciding to invert items 'hidden' state depending 683 | // if they are already mostly hidden 684 | let hide = (hiddenCount >= (chart.data.datasets.length - 1) * 0.5) ? null : true; 685 | 686 | chart.data.datasets.forEach((_, dataSetIndex) => { 687 | if (dataSetIndex === legendItem.datasetIndex) { 688 | return; 689 | } 690 | 691 | let dsMeta = chart.getDatasetMeta(dataSetIndex); 692 | dsMeta.hidden = hide; 693 | }); 694 | 695 | chart.update(); 696 | } 697 | }, 698 | }, 699 | title: { 700 | display: true, 701 | text: titleText, 702 | color: aocColors["main"], 703 | font: { 704 | weight: "normal", 705 | size: 24, 706 | }, 707 | }, 708 | }; 709 | this.scales = { 710 | x: { 711 | title: { 712 | display: true, 713 | text: "Day of Advent", 714 | color: aocColors["main"], 715 | }, 716 | grid: { 717 | color: aocColors["tertiary"], 718 | }, 719 | }, 720 | y: { }, 721 | }; 722 | 723 | if (data.isLargeLeaderboard) { 724 | this.plugins.legend.labels.padding = 4; 725 | this.plugins.legend.labels.boxHeight = 6; 726 | this.plugins.legend.labels.boxWidth = 6; 727 | } 728 | this.plugins.legend.labels.filter = (legendItem, chartData) => { 729 | const dataset = chartData.datasets[legendItem.datasetIndex]; 730 | return dataset.showInLegend; 731 | }; 732 | } 733 | 734 | withOnClick(onClick) { 735 | this.onClick = onClick; 736 | return this; 737 | } 738 | 739 | withTooltips(definition) { 740 | this.plugins = this.plugins || {}; 741 | this.plugins.tooltip = definition; 742 | return this; 743 | } 744 | 745 | withXStackedScale() { 746 | let x = this.scales.x; 747 | x.stacked = true; 748 | x.ticks = { 749 | fontColor: aocColors["main"], 750 | }; 751 | return this; 752 | } 753 | 754 | withXTickingScale() { 755 | let x = this.scales.x; 756 | x.min = 0; 757 | x.max = 25; 758 | x.ticks = { 759 | color: aocColors["main"], 760 | stepSize: 1, 761 | }; 762 | return this; 763 | } 764 | 765 | withXTimeScale(data, { xMax = 0, titleText = "Day of Advent" }) { 766 | let x = this.scales.x; 767 | x.type = "time"; 768 | x.time = { 769 | displayFormats: { 770 | day: 'D', 771 | }, 772 | }; 773 | x.ticks = { 774 | color: aocColors["main"], 775 | stepSize: 1, 776 | }; 777 | x.min = moment([data.year, 10, 30, 17, 0, 0]); 778 | x.max = moment([data.year, 11, xMax || 31, 4, 0, 0]); 779 | x.title.text = titleText; 780 | return this; 781 | } 782 | 783 | withYScale(definition) { 784 | this.scales.y = definition; 785 | this.scales.y.ticks = { 786 | color: aocColors["main"], 787 | }; 788 | return this; 789 | } 790 | } 791 | 792 | class App { 793 | constructor() { 794 | console.info("Constructing App"); 795 | 796 | this.wrapper = document.body.appendChild(document.createElement("div")); 797 | this.controls = this.wrapper.appendChild(document.createElement("div")); 798 | this.medals = this.wrapper.appendChild(document.createElement("div")); 799 | this.perDayLeaderBoard = this.wrapper.appendChild(document.createElement("div")); 800 | this.graphs = this.wrapper.appendChild(document.createElement("div")); 801 | 802 | this.wrapper.id = "aoc-extension"; 803 | this.medals.id = "aoc-extension-medals"; 804 | this.perDayLeaderBoard.id = "aoc-extension-perDayLeaderBoard"; 805 | this.graphs.id = "aoc-extension-graphs"; 806 | 807 | if (isResponsivenessToggled()) { 808 | this.graphs.style.display = "grid"; 809 | } 810 | 811 | this.graphs.style.gridTemplateColumns = "1fr 1fr"; 812 | this.graphs.style.gap = "1rem"; 813 | 814 | if (!getCurrentGraphColorStyle()) 815 | toggleCurrentGraphColorStyle({shouldReload: false}); 816 | 817 | getLeaderboardJson() 818 | .then(data => this.loadControlButtons(data)) 819 | .then(data => this.loadMedalOverview(data)) 820 | .then(data => this.loadPerDayLeaderBoard(data)) 821 | .then(data => this.loadPointsOverTime(data)) 822 | .then(data => this.loadStarsOverTime(data)) 823 | .then(data => this.loadDayVsTime(data)) 824 | .then(data => this.loadTimePerStar(data)) 825 | .then(_data => this.setupFullScreenModes()); 826 | } 827 | 828 | loadHr(data) { 829 | this.controls.appendChild(document.createElement("hr")); 830 | return data; 831 | } 832 | 833 | refreshFullScreenSetup() { 834 | const subject = getFullScreenSubject(); 835 | switch (subject) { 836 | case "medals": 837 | case "perDayLeaderBoard": 838 | case "graphs": 839 | document.body.classList.add('aoc-extension-with-full-screen-overlay'); 840 | this.medals.classList.toggle('aoc-extension-full-screen-subject', subject === "medals"); 841 | this.perDayLeaderBoard.classList.toggle('aoc-extension-full-screen-subject', subject === "perDayLeaderBoard"); 842 | this.graphs.classList.toggle('aoc-extension-full-screen-subject', subject === "graphs"); 843 | break; 844 | 845 | case null: 846 | default: 847 | document.body.classList.remove('aoc-extension-with-full-screen-overlay'); 848 | break; 849 | } 850 | } 851 | 852 | setupFullScreenModes(/** @type {IAppData} */ _data) { 853 | this.refreshFullScreenSetup(); 854 | const exitFullScreenButton = document.createElement("div"); 855 | exitFullScreenButton.className = "aoc-extension-full-screen-exit-button"; 856 | exitFullScreenButton.innerText = "×"; 857 | exitFullScreenButton.addEventListener("click", () => { 858 | setFullScreenSubject(null); 859 | this.refreshFullScreenSetup(); 860 | }); 861 | document.body.appendChild(exitFullScreenButton); 862 | 863 | document.documentElement.addEventListener("click", (event) => { 864 | if (event.target instanceof HTMLHtmlElement) { 865 | setFullScreenSubject(null); 866 | this.refreshFullScreenSetup(); 867 | } 868 | }); 869 | 870 | const fullScreenWarning = document.createElement("div"); 871 | fullScreenWarning.style.display = "none"; 872 | fullScreenWarning.classList.add("aoc-extension-full-screen-warning"); 873 | fullScreenWarning.innerText = "Note: viewing full-screen leaderboard generated by browser extension. Exit with the button top-right."; 874 | document.body.appendChild(fullScreenWarning); 875 | } 876 | 877 | loadControlButtons(/** @type {IAppData} */ data) { 878 | const cacheBustLink = this.controls.appendChild(document.createElement("a")); 879 | cacheBustLink.innerText = "🔄 Clear Charts Cache"; 880 | cacheBustLink.style.cursor = "pointer"; 881 | cacheBustLink.style.background = aocColors.tertiary; 882 | cacheBustLink.style.display = "inline-block"; 883 | cacheBustLink.style.padding = "2px 8px"; 884 | cacheBustLink.style.border = `1px solid ${aocColors.secondary}`; 885 | cacheBustLink.addEventListener("click", () => clearCache()); 886 | 887 | const responsiveToggleLink = this.controls.appendChild(document.createElement("a")); 888 | responsiveToggleLink.innerText = (isResponsivenessToggled() ? "✅" : "❌") + " Graphs in 2x2 grid"; 889 | responsiveToggleLink.title = "Trigger side-by-side graphs if the viewport is wider than 1800px"; 890 | responsiveToggleLink.style.cursor = "pointer"; 891 | responsiveToggleLink.style.background = aocColors.tertiary; 892 | responsiveToggleLink.style.display = "inline-block"; 893 | responsiveToggleLink.style.padding = "2px 8px"; 894 | responsiveToggleLink.style.border = `1px solid ${aocColors.secondary}`; 895 | responsiveToggleLink.style.marginLeft = "8px"; 896 | responsiveToggleLink.addEventListener("click", () => toggleResponsiveness()); 897 | 898 | const colorToggleLink = this.controls.appendChild(document.createElement("a")); 899 | colorToggleLink.innerText = `🎨 Palette: ${getCurrentGraphColorStyle()}`; 900 | colorToggleLink.title = "Cycle through different graph color styles"; 901 | colorToggleLink.style.cursor = "pointer"; 902 | colorToggleLink.style.background = aocColors.tertiary; 903 | colorToggleLink.style.display = "inline-block"; 904 | colorToggleLink.style.padding = "2px 8px"; 905 | colorToggleLink.style.border = `1px solid ${aocColors.secondary}`; 906 | colorToggleLink.style.marginLeft = "8px"; 907 | colorToggleLink.addEventListener("click", () => toggleCurrentGraphColorStyle()); 908 | 909 | const fullScreenButtons = this.controls.appendChild(document.createElement("div")); 910 | fullScreenButtons.innerText = "| Go full screen with: "; 911 | fullScreenButtons.title = "Useful for example to show a permanent leaderboard in your office hallway on a monitor."; 912 | fullScreenButtons.className = "aoc-extension-full-screen-buttons"; 913 | 914 | const medalsButton = document.createElement("a"); 915 | medalsButton.innerText = "🥇"; 916 | medalsButton.className = "aoc-extension-full-screen-button"; 917 | medalsButton.addEventListener("click", () => { 918 | setFullScreenSubject("medals"); 919 | this.refreshFullScreenSetup(); 920 | }); 921 | fullScreenButtons.appendChild(medalsButton); 922 | const perDayLeaderBoardButton = document.createElement("a"); 923 | perDayLeaderBoardButton.innerText = "📆"; 924 | perDayLeaderBoardButton.className = "aoc-extension-full-screen-button"; 925 | perDayLeaderBoardButton.addEventListener("click", () => { 926 | setFullScreenSubject("perDayLeaderBoard"); 927 | this.refreshFullScreenSetup(); 928 | }); 929 | fullScreenButtons.appendChild(perDayLeaderBoardButton); 930 | const graphsButton = document.createElement("a"); 931 | graphsButton.innerText = "📈"; 932 | graphsButton.className = "aoc-extension-full-screen-button"; 933 | graphsButton.addEventListener("click", () => { 934 | setFullScreenSubject("graphs"); 935 | this.refreshFullScreenSetup(); 936 | }); 937 | fullScreenButtons.appendChild(graphsButton); 938 | 939 | return data; 940 | } 941 | 942 | loadPerDayLeaderBoard(/** @type {IAppData} */ data) { 943 | this.perDayLeaderBoard.title = "Delta-focused overviews"; 944 | let titleElement = this.perDayLeaderBoard.appendChild(document.createElement("h3")); 945 | titleElement.innerText = "Delta-focused stats: "; 946 | titleElement.style.fontFamily = "Source Code Pro, monospace"; 947 | titleElement.style.fontWeight = "normal"; 948 | titleElement.style.marginTop = "32px"; 949 | titleElement.style.marginBottom = "8px"; 950 | this.perDayLeaderBoard.style.marginBottom = "32px"; 951 | 952 | let /** @type {number|string|null} */ displayDay = getDisplayDay(); 953 | 954 | if (data.maxDay <= 0) { 955 | let noDataText = this.perDayLeaderBoard.appendChild(document.createElement("p")); 956 | noDataText.innerText = "No data available yet."; 957 | noDataText.style.color = aocColors["secondary"]; 958 | return data; 959 | } 960 | 961 | if (displayDay !== "overview") { 962 | // taking the min to avoid going out of bounds for current year 963 | displayDay = displayDay ? Math.min(parseInt(displayDay), data.maxDay) : data.maxDay; 964 | } 965 | 966 | let tablePerDay = {}, anchorPerDay = {}; 967 | 968 | for (let d = 1; d <= data.maxDay; ++d) { 969 | let a = titleElement.appendChild(document.createElement("a")); 970 | a.dataset["key"] = d.toString(); 971 | a.innerText = " " + d.toString(); 972 | a.addEventListener("click", (evt) => { 973 | // @ts-ignore 974 | const key = evt.target.dataset["key"]; 975 | setDisplayDay(key); 976 | setVisible(key); 977 | }); 978 | a.style.cursor = "pointer"; 979 | anchorPerDay[d] = a; 980 | } 981 | 982 | const divider = titleElement.appendChild(document.createElement("span")); 983 | divider.innerText = " | "; 984 | 985 | const overviewAnchor = titleElement.appendChild(document.createElement("a")); 986 | overviewAnchor.innerText = "Overview" 987 | overviewAnchor.addEventListener("click", () => { 988 | setDisplayDay("overview"); 989 | setVisible("overview"); 990 | }); 991 | overviewAnchor.style.cursor = "pointer"; 992 | overviewAnchor.dataset["key"] = "overview"; 993 | anchorPerDay["overview"] = overviewAnchor; 994 | 995 | function createCell(text, bgColor = "transparent") { 996 | const td = document.createElement("td"); 997 | td.innerText = text; 998 | td.style.border = "1px solid #333"; 999 | td.style.padding = "6px"; 1000 | td.style.textAlign = "center"; 1001 | td.style.backgroundColor = bgColor; 1002 | return td; 1003 | } 1004 | 1005 | function generateOverviewTable() { 1006 | const deltaLeaderBoard = document.createElement("table"); 1007 | tablePerDay["overview"] = deltaLeaderBoard; 1008 | 1009 | deltaLeaderBoard.title = "Delta Leaderboard"; 1010 | 1011 | let table = document.createElement("table"); 1012 | table.style.borderCollapse = "collapse"; 1013 | table.style.fontSize = "16px"; 1014 | 1015 | function createDividerCell() { 1016 | const td = document.createElement("td"); 1017 | td.innerHTML = " "; 1018 | return td; 1019 | } 1020 | 1021 | function createHeaderCell(text, color = "inherit", title = "") { 1022 | const th = document.createElement("th"); 1023 | th.innerText = text; 1024 | th.title = title; 1025 | th.style.padding = "4px 8px"; 1026 | th.style.color = color; 1027 | th.style.textAlign = "center"; 1028 | th.style.cursor = "pointer"; 1029 | return th; 1030 | } 1031 | 1032 | { 1033 | // table header 1034 | let tr = table.appendChild(document.createElement("tr")); 1035 | tr.appendChild(createHeaderCell("")) 1036 | tr.appendChild(createHeaderCell("Stars")); 1037 | tr.appendChild(createHeaderCell("")) 1038 | tr.appendChild(createHeaderCell("Delta Points ⬇", "#ffff66")); 1039 | tr.appendChild(createDividerCell()); 1040 | tr.appendChild(createHeaderCell("Mean*", "#ffff66", "deltas of more than 4 hours are not included")); 1041 | tr.appendChild(createHeaderCell("Median*", "#ffff66", "deltas of more than 4 hours are not included")); 1042 | tr.appendChild(createDividerCell()); 1043 | for (let d = 1; d <= data.maxDay; ++d) { 1044 | let th = tr.appendChild(createHeaderCell(d)); 1045 | th.style.fontSize = "11px"; 1046 | th.style.minWidth = "14px"; 1047 | } 1048 | } 1049 | 1050 | let rank = 0; 1051 | const divider = data.maxDeltaPoints * data.maxDeltaPoints; // Comparing quadratics makes distinctions clearer visually 1052 | for (let member of data.members.slice().sort(deltaPointsTotalSorter).reverse()) { 1053 | const bgColor = member.isLoggedInUser ? aocColors["highlight"] : "transparent"; 1054 | rank += 1; 1055 | let tr = table.appendChild(document.createElement("tr")); 1056 | 1057 | let td = tr.appendChild(document.createElement("td")); 1058 | td.style.textAlign = "left"; 1059 | td.innerText = rank + ". " + member.name; 1060 | td.style.border = "1px solid #333"; 1061 | td.style.padding = "6px"; 1062 | td.style.backgroundColor = member.isLoggedInUser ? aocColors["highlight"] : "transparent"; 1063 | 1064 | let tdStars = tr.appendChild(createCell(member.stars.length)); 1065 | let percent = member.stars.length / 50 * 100; 1066 | tdStars.style.background = member.isLoggedInUser 1067 | ? aocColors["highlight"] 1068 | : `linear-gradient(to right, rgb(255,255,255,0.05) 0%, rgb(255,255,255,0.05) ${percent}%, transparent ${percent}%, transparent 100%)`; 1069 | 1070 | tr.appendChild(createDividerCell()); 1071 | tr.appendChild(createCell(member.deltaPointsTotal, bgColor)); 1072 | tr.appendChild(createDividerCell()); 1073 | tr.appendChild(createCell(member.deltaMeanSeconds ? formatTimeTaken(member.deltaMeanSeconds) : "", bgColor)); 1074 | tr.appendChild(createCell(member.deltaMedianSeconds ? formatTimeTaken(member.deltaMedianSeconds) : "", bgColor)); 1075 | tr.appendChild(createDividerCell()); 1076 | 1077 | for (let d = 1; d <= data.maxDay; ++d) { 1078 | const delta = member.deltas.find(x => x.dayNr === d); 1079 | const td = tr.appendChild(createCell(delta?.points || "")); 1080 | td.title = "Delta time: " + (delta ? formatTimeTaken(delta.deltaTimeTakenSeconds) : "none"); 1081 | td.style.padding = "2px"; 1082 | td.style.fontSize = "11px"; 1083 | td.style.background = `rgba(255,255,255,${delta ? (delta.points * delta.points) / divider / 10 : 0})`; 1084 | } 1085 | } 1086 | 1087 | deltaLeaderBoard.appendChild(table); 1088 | 1089 | return deltaLeaderBoard; 1090 | } 1091 | 1092 | function generateTable(displayDay) { 1093 | let gridElement = document.createElement("table"); 1094 | tablePerDay[displayDay] = gridElement; 1095 | gridElement.style.borderCollapse = "collapse"; 1096 | gridElement.style.fontSize = "16px"; 1097 | 1098 | function sortByDeltaTime(a, b) { 1099 | let a1 = a.stars.find(s => s.dayNr === displayDay && s.starNr === 1); 1100 | let a2 = a.stars.find(s => s.dayNr === displayDay && s.starNr === 2); 1101 | let b1 = b.stars.find(s => s.dayNr === displayDay && s.starNr === 1); 1102 | let b2 = b.stars.find(s => s.dayNr === displayDay && s.starNr === 2); 1103 | if (!a2 && !b2) return 0; 1104 | if (!a2) return 1; 1105 | if (!b2) return -1; 1106 | const aTime = a2.timeTakenSeconds - a1.timeTakenSeconds; 1107 | const bTime = b2.timeTakenSeconds - b1.timeTakenSeconds; 1108 | if (aTime === bTime) return 0; 1109 | return aTime > bTime ? 1 : -1; 1110 | } 1111 | 1112 | function sortByPart(starNr) { 1113 | return function sortByPart2Time(a, b) { 1114 | let aStar = a.stars.find(s => s.dayNr === displayDay && s.starNr === starNr); 1115 | let bStar = b.stars.find(s => s.dayNr === displayDay && s.starNr === starNr); 1116 | if (!aStar) return 1; 1117 | if (!bStar) return -1; 1118 | return aStar.timeTakenSeconds > bStar.timeTakenSeconds ? 1 : -1; 1119 | } 1120 | } 1121 | 1122 | function sortByTotalPoints(a, b) { 1123 | let aPoints = a.stars.filter(s => s.dayNr == displayDay).reduce((acc, v) => acc + v.points, 0); 1124 | let bPoints = b.stars.filter(s => s.dayNr == displayDay).reduce((acc, v) => acc + v.points, 0); 1125 | return bPoints - aPoints; 1126 | } 1127 | 1128 | let grid = data.members; 1129 | let sortFns = { 1130 | "delta": sortByDeltaTime, 1131 | "completion": sortByTotalPoints, 1132 | "part1": sortByPart(1), 1133 | "part2": sortByPart(2), 1134 | }; 1135 | grid.sort(sortFns[Object.keys(sortFns).find(k => k === getTimeTableSort()) || "delta"]); 1136 | 1137 | function createHeaderCell(sorting, text, color = "inherit") { 1138 | const td = document.createElement("td"); 1139 | td.innerText = text; 1140 | td.style.padding = "4px 8px"; 1141 | td.style.color = color; 1142 | td.style.textAlign = "center"; 1143 | td.style.cursor = "pointer"; 1144 | td.addEventListener("click", () => setTimeTableSort(sorting)); 1145 | return td; 1146 | } 1147 | 1148 | { 1149 | // first row header 1150 | let tr = gridElement.appendChild(document.createElement("tr")); 1151 | let th = tr.appendChild(document.createElement("th")) 1152 | 1153 | th = tr.appendChild(createHeaderCell("part1", "----- Part 1 -----", "#9999cc")); 1154 | th.colSpan = 3; 1155 | th = tr.appendChild(createHeaderCell("delta", "----- Delta -----")); 1156 | th.title = "Everyone starting puzzles at different times? See who's the fastest to go from 1 to 2 stars on a day!"; 1157 | th.colSpan = 1; 1158 | th = tr.appendChild(createHeaderCell("part2", "----- Part 2 -----", "#ffff66")); 1159 | th.colSpan = 3; 1160 | th = tr.appendChild(createHeaderCell("total", "----- Total -----")); 1161 | th.colSpan = 1; 1162 | } 1163 | { 1164 | // second row header 1165 | let tr = gridElement.appendChild(document.createElement("tr")); 1166 | let td = tr.appendChild(document.createElement("td")); 1167 | 1168 | // Part 1 1169 | td = tr.appendChild(createHeaderCell("part1", "Time" + (getTimeTableSort() === "part1" ? " ⬇" : ""), "#9999cc")); 1170 | if (getTimeTableSort() === "part1") { 1171 | td.style.color = "#9999ee"; 1172 | td.style.textShadow = "0 0 5px #9999cc"; 1173 | } 1174 | td = tr.appendChild(createHeaderCell("part1", "Rank", "#9999cc")); 1175 | td = tr.appendChild(createHeaderCell("part1", "Points", "#9999cc")); 1176 | 1177 | // Delta 1178 | td = tr.appendChild(createHeaderCell("delta", "Delta Time" + (getTimeTableSort() === "delta" ? " ⬇" : ""))); 1179 | td.title = "Time difference between Part 2 and Part 1"; 1180 | if (getTimeTableSort() === "delta") { 1181 | td.style.color = "#ffffff"; 1182 | td.style.textShadow = "0 0 5px #ffffff"; 1183 | } 1184 | 1185 | // Part 2 1186 | td = tr.appendChild(createHeaderCell("part2", "Time" + (getTimeTableSort() === "part2" ? " ⬇" : ""), "#ffff66")); 1187 | if (getTimeTableSort() === "part2") { 1188 | td.style.color = "#ffff66"; 1189 | td.style.textShadow = "0 0 5px #ffff66"; 1190 | } 1191 | td = tr.appendChild(createHeaderCell("part2", "Rank", "#ffff66")); 1192 | td = tr.appendChild(createHeaderCell("part2", "Points", "#ffff66")); 1193 | 1194 | // Total 1195 | td = tr.appendChild(createHeaderCell("completion", "Points" + (getTimeTableSort() === "completion" ? " ⬇" : ""))); 1196 | if (getTimeTableSort() === "completion") { 1197 | td.style.color = "#ffffff"; 1198 | td.style.textShadow = "0 0 5px #ffffff"; 1199 | } 1200 | } 1201 | 1202 | const maxSecondsForSparkline = 4 /* hours */ * 3600; 1203 | let rank = 0; 1204 | let maxDeltaTime = Math.max.apply(Math, grid 1205 | .map(m => { 1206 | let memberStar1 = m.stars.find(s => s.dayNr === displayDay && s.starNr === 1); 1207 | let memberStar2 = m.stars.find(s => s.dayNr === displayDay && s.starNr === 2); 1208 | const delta = memberStar2 ? memberStar2.timeTakenSeconds - (memberStar1?.timeTakenSeconds || 0) : null; 1209 | return delta || 0 > maxSecondsForSparkline ? null : delta; 1210 | })) 1211 | ; 1212 | 1213 | for (let member of grid) { 1214 | let memberStar1 = member.stars.find(s => s.dayNr === displayDay && s.starNr === 1); 1215 | let memberStar2 = member.stars.find(s => s.dayNr === displayDay && s.starNr === 2); 1216 | 1217 | // skip users that didn't solve any problem today 1218 | if (!memberStar1 && !memberStar2) { 1219 | continue; 1220 | } 1221 | 1222 | rank += 1; 1223 | 1224 | let tr = gridElement.appendChild(document.createElement("tr")); 1225 | if (member.isLoggedInUser) { 1226 | tr.style.backgroundColor = aocColors["highlight"]; 1227 | } 1228 | let td = tr.appendChild(createCell(rank.toString() + ". " + member.name)) 1229 | td.style.textAlign = "left"; 1230 | 1231 | td = tr.appendChild(createCell((memberStar1 ? formatTimeTaken(memberStar1.timeTakenSeconds) : ""))) 1232 | td.title = memberStar1 ? formatStarMomentForTitle(memberStar1) : "Star 1 not done yet"; 1233 | if (getTimeTableSort() === "part1") { 1234 | td.style.color = "#ffffff"; 1235 | td.style.textShadow = "0 0 5px #ffffff"; 1236 | } 1237 | 1238 | td = tr.appendChild(createCell((memberStar1 ? memberStar1.rank : ""))) 1239 | td = tr.appendChild(createCell((memberStar1 ? memberStar1.points : ""))) 1240 | 1241 | td = tr.appendChild(createCell(memberStar2 ? formatTimeTaken(memberStar2.timeTakenSeconds - (memberStar1?.timeTakenSeconds || 0)) : "")); 1242 | if (getTimeTableSort() === "delta") { 1243 | td.style.color = "#ffffff"; 1244 | td.style.textShadow = "0 0 5px #ffffff"; 1245 | } 1246 | 1247 | if (memberStar2 && maxDeltaTime) { 1248 | const delta = memberStar2.timeTakenSeconds - (memberStar1?.timeTakenSeconds || 0); 1249 | const fraction = Math.min(100, delta / maxDeltaTime * 100); 1250 | const sparkline = td.appendChild(document.createElement("div")); 1251 | sparkline.style.height = "2px"; 1252 | sparkline.style.marginTop = "4px"; 1253 | sparkline.style.marginBottom = "1px"; 1254 | sparkline.style.width = `${fraction}%`; 1255 | sparkline.style.backgroundColor = "#ffffff"; 1256 | if (getTimeTableSort() === "delta") { 1257 | sparkline.style.boxShadow = "1px 1px 5px rgba(255, 255, 255, 0.5), -1px -1px 5px rgba(255, 255, 255, 0.5)"; 1258 | } 1259 | sparkline.style.opacity = delta > maxDeltaTime ? "0.15" : "0.75"; 1260 | sparkline.title = "Spark line showing relative 'delta time' values (up to a maximum delta time)"; 1261 | } 1262 | 1263 | td = tr.appendChild(createCell((memberStar2 ? formatTimeTaken(memberStar2.timeTakenSeconds) : ""))) 1264 | td.title = memberStar2 ? formatStarMomentForTitle(memberStar2) : "Star 2 not done yet"; 1265 | if (getTimeTableSort() === "part2") { 1266 | td.style.color = "#ffffff"; 1267 | td.style.textShadow = "0 0 5px #ffffff"; 1268 | } 1269 | 1270 | td = tr.appendChild(createCell((memberStar2 ? memberStar2.rank : ""))) 1271 | td = tr.appendChild(createCell((memberStar2 ? memberStar2.points : ""))) 1272 | 1273 | let totalScore = 0; 1274 | if (memberStar1) { 1275 | totalScore += memberStar1.points; 1276 | } 1277 | if (memberStar2) { 1278 | totalScore += memberStar2.points; 1279 | } 1280 | 1281 | td = tr.appendChild(createCell(totalScore ? totalScore : "0")) 1282 | if (getTimeTableSort() === "completion") { 1283 | td.style.color = "#ffffff"; 1284 | td.style.textShadow = "0 0 5px #ffffff"; 1285 | } 1286 | } 1287 | 1288 | return gridElement; 1289 | } 1290 | 1291 | function setVisible(day) { 1292 | for (const t in tablePerDay) { 1293 | tablePerDay[t].style.display = "none"; 1294 | } 1295 | tablePerDay[day].style.display = "table"; 1296 | 1297 | for (const a in anchorPerDay) { 1298 | anchorPerDay[a].style.color = ""; 1299 | anchorPerDay[a].style.textShadow = ""; 1300 | } 1301 | anchorPerDay[day].style.color = "#ffffff"; 1302 | anchorPerDay[day].style.textShadow = "0 0 5px #ffffff"; 1303 | } 1304 | 1305 | for (let i=1; i <= data.maxDay; i++) { 1306 | this.perDayLeaderBoard.appendChild(generateTable(i)); 1307 | } 1308 | 1309 | this.perDayLeaderBoard.appendChild(generateOverviewTable()); 1310 | 1311 | setVisible(displayDay); 1312 | 1313 | return data; 1314 | } 1315 | 1316 | loadMedalOverview(/** @type {IAppData} */ data) { 1317 | const medalHtml = n => n === 0 ? "🥇" : n === 1 ? "🥈" : n === 2 ? "🥉" : `${n}`; 1318 | const medalColor = n => n === 0 ? "gold" : n === 1 ? "silver" : n === 2 ? "#945210" : "rgba(15, 15, 35, 1.0)"; 1319 | 1320 | // The default font stack of AoC is only the first two, so we add a few to the end here 1321 | // to make sure that systems without the medals in the font will still see them if they 1322 | // are present in the fallback fonts. 1323 | // See also: https://github.com/jeroenheijmans/advent-of-code-charts/issues/56 1324 | const medalFontFamily = '"Source Code Pro", monospace, serif, sans-serif, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji"'; 1325 | 1326 | this.medals.title = 1327 | (isShowAllToggled() 1328 | ? '' 1329 | : 'For each day, the top 3 to get the second star are shown. ') + 1330 | 'Behind each medal you can get a glimpse of the podium for the *first* star.'; 1331 | let titleElement = this.medals.appendChild(document.createElement("h3")); 1332 | titleElement.innerText = "Podium per day: "; 1333 | titleElement.style.fontFamily = "Helvetica, Arial, sans-serif"; 1334 | titleElement.style.fontWeight = "normal"; 1335 | titleElement.style.marginBottom = "4px"; 1336 | 1337 | const showAllToggleLink = titleElement.appendChild(document.createElement("a")); 1338 | showAllToggleLink.innerText = isShowAllToggled() ? "🎄 Showing all participants" : "🥇 Showing only medalists"; 1339 | showAllToggleLink.title = "Toggle between showing only medalists or all participants"; 1340 | showAllToggleLink.style.cursor = "pointer"; 1341 | showAllToggleLink.addEventListener("click", () => toggleShowAll()); 1342 | 1343 | if (data.maxDay <= 0) { 1344 | let noDataText = this.medals.appendChild(document.createElement("p")); 1345 | noDataText.innerText = "No data available yet."; 1346 | noDataText.style.color = aocColors["secondary"]; 1347 | return data; 1348 | } 1349 | 1350 | let gridElement = document.createElement("table"); 1351 | gridElement.style.borderCollapse = "collapse"; 1352 | gridElement.style.fontSize = "16px"; 1353 | 1354 | let grid = data.members; 1355 | 1356 | let tr = gridElement.appendChild(document.createElement("tr")); 1357 | for (let d = 0; d <= 25; d++) { 1358 | let td = tr.appendChild(document.createElement("td")); 1359 | td.innerText = d === 0 ? "" : d.toString(); 1360 | td.align = "center"; 1361 | } 1362 | tr.appendChild(document.createElement("td")); 1363 | for (let n = 0; n < podiumLength; n++) { 1364 | let td = tr.appendChild(document.createElement("td")); 1365 | let span = td.appendChild(document.createElement("span")); 1366 | span.innerText = medalHtml(n); 1367 | span.style.backgroundColor = medalColor(n); 1368 | span.style.padding = "1px"; 1369 | span.style.fontFamily = medalFontFamily; 1370 | td.style.padding = "4px"; 1371 | td.align = "center"; 1372 | } 1373 | 1374 | for (let member of grid.sort(memberByPodiumSorter)) { 1375 | const cellColor = member.isLoggedInUser ? aocColors["highlight"] : "transparent"; 1376 | let tr = document.createElement("tr"); 1377 | let medalCount = 0; 1378 | 1379 | let td = tr.appendChild(document.createElement("td")); 1380 | td.innerText = member.name || "-"; 1381 | td.style.backgroundColor = cellColor; 1382 | td.style.border = "1px solid #333"; 1383 | td.style.padding = "2px 8px"; 1384 | 1385 | for (let d = 1; d <= 25; d++) { 1386 | let td = tr.appendChild(document.createElement("td")); 1387 | td.style.backgroundColor = cellColor; 1388 | td.style.border = "1px solid #333"; 1389 | td.style.padding = "3px 4px"; 1390 | td.style.textAlign = "center"; 1391 | 1392 | let div = td.appendChild(document.createElement("div")); 1393 | div.style.padding = "2px"; 1394 | div.style.minWidth = "24px"; 1395 | div.style.minHeight = "24px"; 1396 | 1397 | if (d <= data.maxDay) { 1398 | let secondPuzzlePodiumPlace = data.days[d].podium.findIndex(n => n.memberId === member.id); 1399 | let firstPuzzlePodiumPlace = data.days[d].podiumFirstPuzzle.findIndex(n => n.memberId === member.id); 1400 | 1401 | if (firstPuzzlePodiumPlace >= 0 && firstPuzzlePodiumPlace < podiumLength) { 1402 | div.style.boxShadow = `inset 2px 2px 0 0 ${medalColor(firstPuzzlePodiumPlace)}, inset -2px -2px 0 0 ${medalColor(firstPuzzlePodiumPlace)}`; 1403 | 1404 | medalCount++; 1405 | } 1406 | 1407 | let span = div.appendChild(document.createElement("span")); 1408 | span.innerText = medalHtml(secondPuzzlePodiumPlace); 1409 | span.style.display = "block"; 1410 | span.style.padding = "1px"; 1411 | span.style.borderRadius = "2px"; 1412 | span.style.border = "1px solid #333"; 1413 | span.style.backgroundColor = medalColor(secondPuzzlePodiumPlace); 1414 | span.style.fontFamily = medalFontFamily; 1415 | 1416 | let memberStar1 = member.stars.find(s => s.dayNr === d && s.starNr === 1); 1417 | let memberStar2 = member.stars.find(s => s.dayNr === d && s.starNr === 2); 1418 | 1419 | td.title = (memberStar1 ? formatStarMomentForTitle(memberStar1) : "Star 1 not done yet") 1420 | + "\n" 1421 | + (memberStar2 ? formatStarMomentForTitle(memberStar2) : "Star 2 not done yet"); 1422 | 1423 | if (secondPuzzlePodiumPlace >= 0 && secondPuzzlePodiumPlace < podiumLength) { 1424 | medalCount++; 1425 | div.style.opacity = `${0.5 + (0.5 * ((podiumLength - secondPuzzlePodiumPlace) / podiumLength))}`; 1426 | } else { 1427 | span.innerText = secondPuzzlePodiumPlace >= 0 ? `${(secondPuzzlePodiumPlace + 1)}` : '\u2003'; 1428 | span.style.opacity = "0.25"; 1429 | } 1430 | } 1431 | } 1432 | 1433 | let separator = tr.appendChild(document.createElement("td")); 1434 | separator.innerText = "\u00A0"; 1435 | 1436 | for (let n = 0; n < podiumLength; n++) { 1437 | let td = tr.appendChild(document.createElement("td")); 1438 | td.innerText = `${member.podiumPlacesPerDay[n]}`; 1439 | td.style.backgroundColor = cellColor; 1440 | td.style.border = "1px solid #333"; 1441 | td.style.padding = "2px 8px"; 1442 | td.align = "center"; 1443 | } 1444 | 1445 | if (isShowAllToggled() || medalCount > 0) { 1446 | gridElement.appendChild(tr); 1447 | } 1448 | } 1449 | 1450 | this.medals.appendChild(gridElement); 1451 | 1452 | return data; 1453 | } 1454 | 1455 | createGraphCanvas(data, title = "") { 1456 | const container = document.createElement("div"); 1457 | container.style.position = "relative"; 1458 | container.style.maxWidth = "1600px"; 1459 | container.style.minWidth = "0px"; 1460 | container.style.minHeight = "0px"; 1461 | this.graphs.appendChild(container); 1462 | const element = container.appendChild(document.createElement("canvas")); 1463 | element.title = title; 1464 | return element; 1465 | } 1466 | 1467 | loadDayVsTime(/** @type {IAppData} */ data) { 1468 | let datasets = data.members.map(m => { 1469 | return { 1470 | label: m.name, 1471 | showInLegend: m.isLoggedInUser || m.rank < largeLeaderboardCutOff, 1472 | order: m.isLoggedInUser ? 0 : 1, // lower one gets drawn on top 1473 | backgroundColor: data.isLargeLeaderboard && !m.isLoggedInUser ? m.colorMuted : m.color, 1474 | borderWidth: 1, 1475 | borderColor: "#000", 1476 | pointRadius: m.radius * 2, 1477 | pointStyle: m.pointStyle, 1478 | data: m.stars.map(s => { 1479 | return { 1480 | x: s.dayNr + s.starNr / 2 - 1, 1481 | y: s.timeTaken 1482 | }; 1483 | }) 1484 | }; 1485 | }); 1486 | 1487 | let element = this.createGraphCanvas(data, "Log10 function of the time taken for each user to get the stars"); 1488 | 1489 | let chart = new Chart(element.getContext("2d"), { 1490 | type: "scatter", 1491 | data: { 1492 | datasets: datasets, 1493 | }, 1494 | options: new ChartOptions(data, "Stars vs Log10(minutes taken per star)") 1495 | .withTooltips({ 1496 | callbacks: { 1497 | label: (item) => { 1498 | const day = Math.floor(Number(item.parsed?.x || 0) + 0.5); 1499 | const star = Number(item.parsed?.x || 0) < day ? 1 : 2; 1500 | const mins = item.parsed?.y; 1501 | 1502 | return `Day ${day} star ${star} took ${mins} minutes to complete`; 1503 | }, 1504 | }, 1505 | }) 1506 | .withXTickingScale() 1507 | .withYScale({ 1508 | type: "logarithmic", 1509 | ticks: { 1510 | fontColor: aocColors["main"], 1511 | }, 1512 | grid: { 1513 | color: aocColors["tertiary"], 1514 | }, 1515 | }) 1516 | }); 1517 | 1518 | return data; 1519 | } 1520 | 1521 | loadTimePerStar(/** @type {IAppData} */ data) { 1522 | let datasets = []; 1523 | let relevantMembers = data.members.sort((a, b) => b.score - a.score); 1524 | 1525 | relevantMembers.forEach( (member, idx) => { 1526 | let star1DataSet = { 1527 | label: `${member.name} (★)`, 1528 | showInLegend: member.isLoggedInUser || member.rank < 10, 1529 | order: member.isLoggedInUser ? 0 : 1, // lower one gets drawn on top 1530 | pointStyle: member.pointStyle, 1531 | stack: `Stack ${member.name}`, 1532 | backgroundColor: member.color, 1533 | borderColor: "rgba(0, 0, 0, 0.5)", 1534 | borderWidth: 1, 1535 | data: /** @type {number[]} */ ([]), 1536 | hidden: data.loggedInUserIsPresumablyKnown ? !member.isLoggedInUser : idx >= 3, 1537 | }; 1538 | 1539 | let star2DataSet = { 1540 | label: `${member.name} (★★)`, 1541 | showInLegend: member.isLoggedInUser || member.rank < 10, 1542 | order: member.isLoggedInUser ? 0 : 1, // lower one gets drawn on top 1543 | pointStyle: member.pointStyle, 1544 | stack: `Stack ${member.name}`, 1545 | backgroundColor: member.colorMuted, 1546 | borderColor: "rgba(0, 0, 0, 0.5)", 1547 | borderWidth: 1, 1548 | data: /** @type {number[]} */ ([]), 1549 | hidden: data.loggedInUserIsPresumablyKnown ? !member.isLoggedInUser : idx >= 3, 1550 | }; 1551 | 1552 | for (let i = 1; i <= 25; i++) { 1553 | let star1 = data.stars.find(s => s.memberId === member.id && s.dayNr === i && s.starKey === "1"); 1554 | let star2 = data.stars.find(s => s.memberId === member.id && s.dayNr === i && s.starKey === "2"); 1555 | 1556 | star1DataSet.data.push(!!star1 ? star1.timeTaken : 0); 1557 | star2DataSet.data.push(!!star2 ? star2.timeTaken - (star1?.timeTaken || 0) : 0); 1558 | } 1559 | 1560 | datasets.push(star1DataSet); 1561 | datasets.push(star2DataSet); 1562 | }); 1563 | 1564 | let element = this.createGraphCanvas(data, "From the top players, show the number of minutes taken each day. (Exclude results over 4 hours.) (Toggle Responsive for all users)"); 1565 | 1566 | let options = new ChartOptions(data, `Minutes taken per star`) 1567 | .withYScale({ 1568 | max: 240, 1569 | ticks: { 1570 | fontColor: aocColors["main"], 1571 | }, 1572 | grid: { 1573 | color: aocColors["tertiary"], 1574 | }, 1575 | }); 1576 | options.plugins.legend.title.text = "Top players"; 1577 | let chart = new Chart(element.getContext("2d"), { 1578 | type: "bar", 1579 | data: { 1580 | labels: range(1, 26), 1581 | datasets: datasets, 1582 | }, 1583 | options, 1584 | }); 1585 | 1586 | return data; 1587 | } 1588 | 1589 | /** 1590 | * @param {IData} data 1591 | */ 1592 | loadPointsOverTime(/** @type {IAppData} */ data) { 1593 | const graphType = getPointsOverTimeType(); 1594 | const maxDayNr = Math.max(...data.stars.map(s => s.dayNr)); 1595 | const maxPointsPerDay = Array.from({ length: maxDayNr }, () => data.n_members * 2); 1596 | 1597 | // TODO: Perhaps do this while parsing so other graphs may use it? 1598 | data.stars.forEach(s => maxPointsPerDay[s.dayNr-1] = s.points > 0 ? data.n_members * 2 : 0); 1599 | const availablePoints = [maxPointsPerDay.map(p => p/2), maxPointsPerDay.map(p => p/2)]; 1600 | data.stars.forEach(s => availablePoints[s.starNr-1][s.dayNr-1] = Math.min(availablePoints[s.starNr-1][s.dayNr-1], Math.max(s.points-1, 0))); 1601 | 1602 | let datasets = data.members.sort((a, b) => a.name?.localeCompare(b.name || "") || 0).reduce((p, m) => { 1603 | const days = m.stars.reduce( 1604 | (map, s) => { 1605 | const current = map.get(s.dayNr) ?? { stars: [], points:0 }; 1606 | return map.set(s.dayNr, { 1607 | stars: [...current.stars, s], 1608 | points: current.points + s.points 1609 | }); 1610 | }, 1611 | new Map() 1612 | ); 1613 | 1614 | if (graphType === 2 && m.stars.length < maxPointsPerDay.length * 2) { 1615 | p.push({ 1616 | label: m.name + ' (potential)', 1617 | showInLegend: m.isLoggedInUser || m.rank < (largeLeaderboardCutOff / 2), 1618 | order: m.isLoggedInUser ? 0 : 1, // lower one gets drawn on top 1619 | lineTension: 0.1, 1620 | fill: false, 1621 | borderWidth: m.borderWidth, 1622 | borderColor: data.isLargeLeaderboard && !m.isLoggedInUser ? m.colorMuted : m.color, 1623 | borderDash: [1, 4], 1624 | backgroundColor: data.isLargeLeaderboard && !m.isLoggedInUser ? m.colorMuted : m.color, 1625 | pointStyle: m.pointStyle, 1626 | pointBorderWidth: 0.5, 1627 | pointBackgroundColor: "transparent", 1628 | data: maxPointsPerDay 1629 | .map((max, i) => ({ 1630 | dayNr: i + 1, 1631 | ...(days.get(i + 1) ?? { stars: [], points: 0 }) 1632 | })) 1633 | .map((day,i) => ({ 1634 | ...day, 1635 | points: day.stars.length === 2 1636 | ? day.points 1637 | : day.stars.length === 1 1638 | ? (day.points + availablePoints[1][i]) 1639 | : (availablePoints[0][i] + availablePoints[1][i]) 1640 | })) 1641 | .map((day, i, days) => ({ 1642 | ...day, 1643 | pointsToDay: days.filter(d => d.dayNr <= day.dayNr).map(d => d.points).reduce((a,b) => a+b), 1644 | maxPointsToDay: maxPointsPerDay.slice(0, day.dayNr).reduce((p,max) => p+(max ?? 0), 0) 1645 | })) 1646 | .filter((day, i, days) => !days.filter(d => d.dayNr <= day.dayNr + 1).every(d => d.stars.length === 2)) 1647 | .map((day) => ({ 1648 | x: moment([data.year, 10, 30, 0, 0, 0]).add(day.dayNr, "d"), 1649 | y: day.pointsToDay / day.maxPointsToDay * 100, 1650 | day 1651 | })), 1652 | }); 1653 | } 1654 | 1655 | p.push({ 1656 | label: m.name, 1657 | showInLegend: m.isLoggedInUser || m.rank < (largeLeaderboardCutOff / (graphType === 2 ? 2 : 1)), 1658 | order: m.isLoggedInUser ? 0 : 1, // lower one gets drawn on top 1659 | lineTension: 0.1, 1660 | fill: false, 1661 | borderWidth: m.borderWidth, 1662 | borderColor: data.isLargeLeaderboard && !m.isLoggedInUser ? m.colorMuted : m.color, 1663 | radius: m.radius, 1664 | pointStyle: m.pointStyle, 1665 | backgroundColor: data.isLargeLeaderboard && !m.isLoggedInUser ? m.colorMuted : m.color, 1666 | data: graphType !== 0 1667 | ? maxPointsPerDay 1668 | .map((max, i) => ({ 1669 | dayNr: i+1, 1670 | ...(days.get(i+1) ?? { stars: [], points:0 }) 1671 | })) 1672 | .map((day, i, days) => ({ 1673 | ...day, 1674 | pointsToDay: days.filter(d => d.dayNr <= day.dayNr).map(d => d.points).reduce((a,b) => a+b), 1675 | maxPointsToDay: maxPointsPerDay.slice(0, day.dayNr).reduce((p,max) => p+(max ?? 0), 0) 1676 | })) 1677 | .map((day) => ({ 1678 | x: moment([data.year, 10, 30, 0, 0, 0]).add(day.dayNr, "d"), 1679 | y: day.pointsToDay / day.maxPointsToDay * 100, 1680 | day 1681 | })) 1682 | : m.stars.filter(s => s.starNr === 2).map(s => { 1683 | return { 1684 | x: s.getStarMoment, 1685 | y: s.nrOfPointsAfterThisOne, 1686 | star: s 1687 | } 1688 | }) 1689 | }); 1690 | return p; 1691 | }, /** @type any[] */ ([])); 1692 | 1693 | const element = this.createGraphCanvas(data, "Points over time per member."); 1694 | 1695 | let chart = new Chart(element.getContext("2d"), { 1696 | type: "line", 1697 | data: { 1698 | datasets: datasets, 1699 | }, 1700 | plugins: [{ 1701 | // See https://stackoverflow.com/a/75034834/419956 by user @LeeLenalee 1702 | afterEvent: (chart, evt) => { 1703 | const { event: { type, x, y, } } = evt; 1704 | if (type !== 'click') return; 1705 | const { titleBlock: { top, right, bottom, left, } } = chart; 1706 | if (left <= x && x <= right && bottom >= y && y >= top) { 1707 | togglePointsOverTimeType() 1708 | } 1709 | } 1710 | }], 1711 | options: new ChartOptions(data, `Points per Day - 🖱️ ${pointsOverTimeType[graphType]}`) 1712 | .withTooltips({ 1713 | callbacks: { 1714 | afterLabel: (item) => { 1715 | if (graphType !== 0) { 1716 | const day = item.dataset.data[item.dataIndex].day; 1717 | return `(day ${day.dayNr}. Total: ${day.pointsToDay} of ${day.maxPointsToDay} points. Today: ${day.points} points, ranked ${day.stars.map(s => `${s.rank}.`).join(' and ') || '-'})`; 1718 | } 1719 | const star = item.dataset.data[item.dataIndex].star; 1720 | return `(completed day ${star.dayNr} star ${star.starNr})`; 1721 | }, 1722 | }, 1723 | }) 1724 | .withXTimeScale(data, { xMax: 25 }) 1725 | .withYScale({ 1726 | ticks: { 1727 | min: 0, 1728 | fontColor: aocColors["main"], 1729 | }, 1730 | scaleLabel: { 1731 | display: true, 1732 | labelString: "cumulative points", 1733 | fontColor: aocColors["main"], 1734 | }, 1735 | grid: { 1736 | color: aocColors["tertiary"], 1737 | zeroLineColor: aocColors["secondary"], 1738 | }, 1739 | }) 1740 | }); 1741 | 1742 | return data; 1743 | } 1744 | 1745 | loadStarsOverTime(/** @type {IAppData} */ data) { 1746 | let datasets = data.members.map(m => { 1747 | return { 1748 | label: m.name, 1749 | showInLegend: m.isLoggedInUser || m.rank < largeLeaderboardCutOff, 1750 | order: m.isLoggedInUser ? 0 : 1, // lower one gets drawn on top 1751 | lineTension: 0.2, 1752 | fill: false, 1753 | borderWidth: m.borderWidth, 1754 | borderColor: data.isLargeLeaderboard && !m.isLoggedInUser ? m.colorMuted : m.color, 1755 | radius: m.radius, 1756 | pointStyle: m.pointStyle, 1757 | backgroundColor: data.isLargeLeaderboard && !m.isLoggedInUser ? m.colorMuted : m.color, 1758 | data: m.stars.filter(s => s.starNr === 2).map(s => { 1759 | return { 1760 | x: s.getStarMoment, 1761 | y: s.nrOfStarsAfterThisOne, 1762 | star: s 1763 | }; 1764 | }), 1765 | } 1766 | }); 1767 | 1768 | let element = this.createGraphCanvas(data, "Number of stars over time per member."); 1769 | 1770 | let chart = new Chart(element.getContext("2d"), { 1771 | type: "line", 1772 | data: { 1773 | datasets: datasets, 1774 | }, 1775 | options: new ChartOptions(data, "Leaderboard (stars)") 1776 | .withTooltips({ 1777 | callbacks: { 1778 | afterLabel: (item) => { 1779 | const star = item.dataset.data[item.dataIndex].star; 1780 | return `(day ${star.dayNr} star ${star.starNr})`; 1781 | }, 1782 | }, 1783 | }) 1784 | .withXTimeScale(data, { xMax: 31, titleText: "December" }) 1785 | .withYScale({ 1786 | ticks: { 1787 | stepSize: 1, 1788 | min: 0, 1789 | fontColor: aocColors["main"], 1790 | }, 1791 | scaleLabel: { 1792 | display: true, 1793 | labelString: "nr of stars", 1794 | fontColor: aocColors["main"], 1795 | }, 1796 | grid: { 1797 | color: aocColors["tertiary"], 1798 | zeroLineColor: aocColors["secondary"], 1799 | }, 1800 | }) 1801 | }); 1802 | 1803 | return data; 1804 | } 1805 | } 1806 | 1807 | aoc["App"] = App; 1808 | 1809 | function loadAdditions() { 1810 | console.info("Going to construct App"); 1811 | new aoc.App(); 1812 | } 1813 | 1814 | if (document.readyState === "complete" || document.readyState === "interactive") { 1815 | console.info(`Loading via readyState = ${document.readyState}`); 1816 | loadAdditions(); 1817 | } else { 1818 | console.info(`Loading via DOMContentLoaded because readyState = ${document.readyState}`); 1819 | document.addEventListener("DOMContentLoaded", () => loadAdditions()); 1820 | } 1821 | 1822 | }(window["aoc"] = window["aoc"] || {})); 1823 | -------------------------------------------------------------------------------- /src/js/dummyData-default.js: -------------------------------------------------------------------------------- 1 | (function (aoc) { 2 | aoc["dummyData"] = { 3 | "members": { 4 | "100001": { 5 | "last_star_ts": "1544190334", 6 | "global_score": 0, 7 | "completion_day_level": { 8 | "1": { 9 | "1": { 10 | "star_index": 1543675444, 11 | "get_star_ts": 1543675444, 12 | }, 13 | "2": { 14 | "star_index": 1543675853, 15 | "get_star_ts": 1543675853, 16 | } 17 | }, 18 | "2": { 19 | "1": { 20 | "star_index": 1544048513, 21 | "get_star_ts": 1544048513, 22 | }, 23 | "2": { 24 | "star_index": 1544088513, 25 | "get_star_ts": 1544088513, 26 | } 27 | }, 28 | "3": { 29 | "1": { 30 | "star_index": 1543835226, 31 | "get_star_ts": 1543835226, 32 | }, 33 | "2": { 34 | "star_index": 1543839592, 35 | "get_star_ts": 1543839592, 36 | } 37 | }, 38 | "4": { 39 | "1": { 40 | "star_index": 1544101317, 41 | "get_star_ts": 1544101317, 42 | }, 43 | "2": { 44 | "star_index": 1544181317, 45 | "get_star_ts": 1544181317, 46 | } 47 | }, 48 | "5": { 49 | "1": { 50 | "star_index": 1544001317, 51 | "get_star_ts": 1544001317, 52 | }, 53 | "2": { 54 | "star_index": 1544005435, 55 | "get_star_ts": 1544005435, 56 | } 57 | }, 58 | "6": { 59 | "1": { 60 | "star_index": 1544088513, 61 | "get_star_ts": 1544088513, 62 | }, 63 | "2": { 64 | "star_index": 1544088923, 65 | "get_star_ts": 1544088923, 66 | } 67 | }, 68 | "7": { 69 | "1": { 70 | "star_index": 1544190334, 71 | "get_star_ts": 1544190334, 72 | } 73 | } 74 | }, 75 | "local_score": 262, 76 | "name": "Master Puzzlebreaker", 77 | "stars": 13, 78 | "id": "100001" 79 | }, 80 | "100002": { 81 | "local_score": 388, 82 | "completion_day_level": { 83 | // Explicitly "not competing" this year, but on the leaderboard 84 | }, 85 | "global_score": 0, 86 | "last_star_ts": "1544180362", 87 | "id": "100002", 88 | "stars": 14, 89 | "name": "General Codestorm" 90 | }, 91 | "100003": { 92 | "last_star_ts": "1543727939", 93 | "completion_day_level": { 94 | "1": { 95 | "1": { 96 | "star_index": 1543640672, 97 | "get_star_ts": 1543640672, 98 | }, 99 | "2": { 100 | "star_index": 1543640962, 101 | "get_star_ts": 1543640962, 102 | } 103 | }, 104 | "2": { 105 | "1": { 106 | "star_index": 1543727391, 107 | "get_star_ts": 1543727391, 108 | }, 109 | "2": { 110 | "star_index": 1543727939, 111 | "get_star_ts": 1543727939, 112 | } 113 | } 114 | }, 115 | "local_score": 129, 116 | "global_score": 0, 117 | "name": "happygirl", 118 | "stars": 4, 119 | "id": "100003" 120 | }, 121 | "100004": { 122 | "completion_day_level": { 123 | "1": { 124 | "1": { 125 | "star_index": 1543640661, 126 | "get_star_ts": 1543640661, 127 | }, 128 | "2": { 129 | "star_index": 1543642266, 130 | "get_star_ts": 1543642266, 131 | } 132 | }, 133 | "2": { 134 | "1": { 135 | "star_index": 1543727017, 136 | "get_star_ts": 1543727017, 137 | }, 138 | "2": { 139 | "star_index": 1543727501, 140 | "get_star_ts": 1543727501, 141 | } 142 | }, 143 | "3": { 144 | "1": { 145 | "star_index": 1543813968, 146 | "get_star_ts": 1543813968, 147 | }, 148 | "2": { 149 | "star_index": 1543814094, 150 | "get_star_ts": 1543814094, 151 | } 152 | }, 153 | "4": { 154 | "1": { 155 | "star_index": 1543901590, 156 | "get_star_ts": 1543901590, 157 | }, 158 | "2": { 159 | "star_index": 1543901693, 160 | "get_star_ts": 1543901693, 161 | } 162 | }, 163 | "5": { 164 | "1": { 165 | "star_index": 1543986872, 166 | "get_star_ts": 1543986872, 167 | }, 168 | "2": { 169 | "star_index": 1543987387, 170 | "get_star_ts": 1543987387, 171 | } 172 | }, 173 | "6": { 174 | "1": { 175 | "star_index": 1544074188, 176 | "get_star_ts": 1544074188, 177 | }, 178 | "2": { 179 | "star_index": 1544074602, 180 | "get_star_ts": 1544074602, 181 | } 182 | }, 183 | "7": { 184 | "1": { 185 | "star_index": 1544161008, 186 | "get_star_ts": 1544161008, 187 | }, 188 | "2": { 189 | "star_index": 1544161099, 190 | "get_star_ts": 1544161099, 191 | } 192 | } 193 | }, 194 | "local_score": 499, 195 | "global_score": 0, 196 | "last_star_ts": "1544161099", 197 | "id": "100004", 198 | "name": "Missk3nduct0r", 199 | "stars": 14 200 | }, 201 | "100005": { 202 | "stars": 13, 203 | "name": "Private Johnson", 204 | "id": "100005", 205 | "last_star_ts": "1544191498", 206 | "completion_day_level": { 207 | "1": { 208 | "1": { 209 | "star_index": 1543659635, 210 | "get_star_ts": 1543640433, 211 | }, 212 | "2": { 213 | "star_index": 1543660577, 214 | "get_star_ts": 1543660577, 215 | } 216 | }, 217 | "2": { 218 | "1": { 219 | "star_index": 1543741662, 220 | "get_star_ts": 1543741662, 221 | }, 222 | "2": { 223 | "star_index": 1543742697, 224 | "get_star_ts": 1543742697, 225 | } 226 | }, 227 | "3": { 228 | "1": { 229 | "star_index": 1543827875, 230 | "get_star_ts": 1543827875, 231 | }, 232 | "2": { 233 | "star_index": 1543833911, 234 | "get_star_ts": 1543833911, 235 | } 236 | }, 237 | "4": { 238 | "1": { 239 | "star_index": 1543914741, 240 | "get_star_ts": 1543914741, 241 | }, 242 | "2": { 243 | "star_index": 1543918162, 244 | "get_star_ts": 1543918162, 245 | } 246 | }, 247 | "5": { 248 | "1": { 249 | "star_index": 1544002117, 250 | "get_star_ts": 1544002117, 251 | }, 252 | "2": { 253 | "star_index": 1544002431, 254 | "get_star_ts": 1544002431, 255 | } 256 | }, 257 | "6": { 258 | "1": { 259 | "star_index": 1544098071, 260 | "get_star_ts": 1544098071, 261 | }, 262 | "2": { 263 | "star_index": 1544098798, 264 | "get_star_ts": 1544098798, 265 | } 266 | }, 267 | "7": { 268 | "1": { 269 | "star_index": 1544191498, 270 | "get_star_ts": 1544191498, 271 | } 272 | } 273 | }, 274 | "global_score": 0, 275 | "local_score": 233 276 | }, 277 | "100006": { 278 | "name": "biti", 279 | "stars": 14, 280 | "id": "100006", 281 | "last_star_ts": "1544164899", 282 | "completion_day_level": { 283 | "1": { 284 | "1": { 285 | "star_index": 1543640432, 286 | "get_star_ts": 1543640433, 287 | }, 288 | "2": { 289 | "star_index": 1543640570, 290 | "get_star_ts": 1543640570, 291 | } 292 | }, 293 | "2": { 294 | "1": { 295 | "star_index": 1543726958, 296 | "get_star_ts": 1543726958, 297 | }, 298 | "2": { 299 | "star_index": 1543727111, 300 | "get_star_ts": 1543727111, 301 | } 302 | }, 303 | "3": { 304 | "1": { 305 | "star_index": 1543813702, 306 | "get_star_ts": 1543813702, 307 | }, 308 | "2": { 309 | "star_index": 1543814067, 310 | "get_star_ts": 1543814067, 311 | } 312 | }, 313 | "4": { 314 | "1": { 315 | "star_index": 1543900950, 316 | "get_star_ts": 1543900950, 317 | }, 318 | "2": { 319 | "star_index": 1543901477, 320 | "get_star_ts": 1543901477, 321 | } 322 | }, 323 | "5": { 324 | "1": { 325 | "star_index": 1543986501, 326 | "get_star_ts": 1543986501, 327 | }, 328 | "2": { 329 | "star_index": 1543986773, 330 | "get_star_ts": 1543986773, 331 | } 332 | }, 333 | "6": { 334 | "1": { 335 | "star_index": 1544078401, 336 | "get_star_ts": 1544078401, 337 | }, 338 | "2": { 339 | "star_index": 1544086872, 340 | "get_star_ts": 1544086872, 341 | } 342 | }, 343 | "7": { 344 | "1": { 345 | "star_index": 1544160058, 346 | "get_star_ts": 1544160058, 347 | }, 348 | "2": { 349 | "star_index": 1544164899, 350 | "get_star_ts": 1544164899, 351 | } 352 | } 353 | }, 354 | "local_score": 527, 355 | "global_score": 312 356 | }, 357 | "100007": { 358 | "last_star_ts": "1543817884", 359 | "completion_day_level": { 360 | "1": { 361 | "1": { 362 | "star_index": 1543640910, 363 | "get_star_ts": 1543640910, 364 | }, 365 | "2": { 366 | "star_index": 1543642654, 367 | "get_star_ts": 1543642654, 368 | } 369 | }, 370 | "2": { 371 | "1": { 372 | "star_index": 1543727866, 373 | "get_star_ts": 1543727866, 374 | }, 375 | "2": { 376 | "star_index": 1543728460, 377 | "get_star_ts": 1543728460, 378 | } 379 | }, 380 | "3": { 381 | "1": { 382 | "star_index": 1543817038, 383 | "get_star_ts": 1543817038, 384 | }, 385 | "2": { 386 | "star_index": 1543817884, 387 | "get_star_ts": 1543817884, 388 | } 389 | } 390 | }, 391 | "local_score": 145, 392 | "global_score": 0, 393 | "name": "Gerard of Spacentockernstrasse", 394 | "stars": 6, 395 | "id": "100007" 396 | }, 397 | "190664": { 398 | "name": "Jeroen Heijmans", 399 | "stars": 14, 400 | "id": "190664", 401 | "last_star_ts": "1544165499", 402 | "local_score": 413, 403 | "completion_day_level": { 404 | "1": { 405 | "1": { 406 | "star_index": 1543640753, 407 | "get_star_ts": 1543640753, 408 | }, 409 | "2": { 410 | "star_index": 1543641238, 411 | "get_star_ts": 1543641238, 412 | } 413 | }, 414 | "2": { 415 | "1": { 416 | "star_index": 1543728583, 417 | "get_star_ts": 1543728583, 418 | }, 419 | "2": { 420 | "star_index": 1543728929, 421 | "get_star_ts": 1543728929, 422 | } 423 | }, 424 | "3": { 425 | "1": { 426 | "star_index": 1543814603, 427 | "get_star_ts": 1543814603, 428 | }, 429 | "2": { 430 | "star_index": 1543814908, 431 | "get_star_ts": 1543814908, 432 | } 433 | }, 434 | "4": { 435 | "1": { 436 | "star_index": 1543902019, 437 | "get_star_ts": 1543902019, 438 | }, 439 | "2": { 440 | "star_index": 1543902182, 441 | "get_star_ts": 1543902182, 442 | } 443 | }, 444 | "5": { 445 | "1": { 446 | "star_index": 1543988033, 447 | "get_star_ts": 1543988033, 448 | }, 449 | "2": { 450 | "star_index": 1543988862, 451 | "get_star_ts": 1543988862, 452 | } 453 | }, 454 | "6": { 455 | "1": { 456 | "star_index": 1544080254, 457 | "get_star_ts": 1544080254, 458 | }, 459 | "2": { 460 | "star_index": 1544081946, 461 | "get_star_ts": 1544081946, 462 | } 463 | }, 464 | "7": { 465 | "1": { 466 | "star_index": 1544160010, 467 | "get_star_ts": 1544160010, 468 | }, 469 | "2": { 470 | "star_index": 1544165499, 471 | "get_star_ts": 1544165499, 472 | } 473 | } 474 | }, 475 | "global_score": 0 476 | }, 477 | "100008": { 478 | "completion_day_level": { 479 | "1": { 480 | "1": { 481 | "star_index": 1543677435, 482 | "get_star_ts": 1543677435, 483 | }, 484 | "2": { 485 | "star_index": 1543678080, 486 | "get_star_ts": 1543678080, 487 | } 488 | }, 489 | "2": { 490 | "1": { 491 | "star_index": 1543733508, 492 | "get_star_ts": 1543733508, 493 | }, 494 | "2": { 495 | "star_index": 1543763332, 496 | "get_star_ts": 1543763332, 497 | } 498 | }, 499 | "3": { 500 | "1": { 501 | "star_index": 1543813611, 502 | "get_star_ts": 1543813611, 503 | }, 504 | "2": { 505 | "star_index": 1543813890, 506 | "get_star_ts": 1543813890, 507 | } 508 | }, 509 | "4": { 510 | "1": { 511 | "star_index": 1543901390, 512 | "get_star_ts": 1543901390, 513 | }, 514 | "2": { 515 | "star_index": 1543901623, 516 | "get_star_ts": 1543901623, 517 | } 518 | }, 519 | "5": { 520 | "1": { 521 | "star_index": 1543986743, 522 | "get_star_ts": 1543986743, 523 | }, 524 | "2": { 525 | "star_index": 1543987118, 526 | "get_star_ts": 1543987118, 527 | } 528 | }, 529 | "6": { 530 | "1": { 531 | "star_index": 1544117396, 532 | "get_star_ts": 1544117396, 533 | }, 534 | "2": { 535 | "star_index": 1544117777, 536 | "get_star_ts": 1544117777, 537 | } 538 | } 539 | }, 540 | "local_score": 302, 541 | "global_score": 0, 542 | "last_star_ts": "1544117777", 543 | "id": "100008", 544 | "stars": 12, 545 | "name": null 546 | }, 547 | "100009": { 548 | "completion_day_level": { 549 | "1": { 550 | "1": { 551 | "star_index": 1543689654, 552 | "get_star_ts": 1543689654, 553 | }, 554 | "2": { 555 | "star_index": 1543711600, 556 | "get_star_ts": 1543711600, 557 | } 558 | } 559 | }, 560 | "global_score": 0, 561 | "local_score": 9, 562 | "last_star_ts": "1543711600", 563 | "id": "100009", 564 | "name": "Karper Waylonton", 565 | "stars": 2 566 | }, 567 | "100010": { 568 | "id": "100010", 569 | "local_score": 45, 570 | "stars": 6, 571 | "global_score": 0, 572 | "name": "Fabelstad der Langnamepeopleston", 573 | "last_star_ts": "1544127420", 574 | "completion_day_level": { 575 | "1": { 576 | "1": { 577 | "star_index": 1543640423, 578 | "get_star_ts": 1543640423, 579 | }, 580 | "2": { 581 | "star_index": 1543640520, 582 | "get_star_ts": 1543640520, 583 | } 584 | }, 585 | "2": { 586 | "1": { 587 | "star_index": 1543777877, 588 | "get_star_ts": 1543777877, 589 | } 590 | }, 591 | "4": { 592 | "1": { 593 | "star_index": 1543960344, 594 | "get_star_ts": 1543960344, 595 | } 596 | }, 597 | "5": { 598 | "1": { 599 | "star_index": 1544125736, 600 | "get_star_ts": 1544125736, 601 | }, 602 | "2": { 603 | "star_index": 1544125836, 604 | "get_star_ts": 1544125836, 605 | } 606 | } 607 | } 608 | }, 609 | "100011": { 610 | "completion_day_level": { 611 | "1": { 612 | "1": { 613 | "star_index": 1543640479, 614 | "get_star_ts": 1543640479, 615 | }, 616 | "2": { 617 | "star_index": 1543640680, 618 | "get_star_ts": 1543640680, 619 | } 620 | }, 621 | "2": { 622 | "1": { 623 | "star_index": 1543732801, 624 | "get_star_ts": 1543732801, 625 | }, 626 | "2": { 627 | "star_index": 1543732884, 628 | "get_star_ts": 1543732884, 629 | } 630 | }, 631 | "3": { 632 | "1": { 633 | "star_index": 1543813463, 634 | "get_star_ts": 1543813463, 635 | }, 636 | "2": { 637 | "star_index": 1544013693, 638 | "get_star_ts": 1544013693, 639 | } 640 | }, 641 | "4": { 642 | "1": { 643 | "star_index": 1543902386, 644 | "get_star_ts": 1543902386, 645 | }, 646 | "2": { 647 | "star_index": 1543902424, 648 | "get_star_ts": 1543902424, 649 | } 650 | }, 651 | "5": { 652 | "1": { 653 | "star_index": 1543986567, 654 | "get_star_ts": 1543986567, 655 | }, 656 | "2": { 657 | "star_index": 1543986871, 658 | "get_star_ts": 1543986871, 659 | } 660 | }, 661 | "6": { 662 | "1": { 663 | "star_index": 1544073487, 664 | "get_star_ts": 1544073487, 665 | }, 666 | "2": { 667 | "star_index": 1544073782, 668 | "get_star_ts": 1544073782, 669 | } 670 | }, 671 | "7": { 672 | "1": { 673 | "star_index": 1544160180, 674 | "get_star_ts": 1544160180, 675 | }, 676 | "2": { 677 | "star_index": 1544161319, 678 | "get_star_ts": 1544161319, 679 | } 680 | } 681 | }, 682 | "global_score": 192, 683 | "local_score": 484, 684 | "last_star_ts": "1544161319", 685 | "id": "100011", 686 | "name": "宮本 茂", 687 | "stars": 14 688 | }, 689 | "100015": { 690 | "id": "372904", 691 | "stars": 14, 692 | "name": "Gainer Z", 693 | "completion_day_level": { 694 | "1": { 695 | "1": { 696 | "star_index": 1543690540, 697 | "get_star_ts": 1543690540, 698 | }, 699 | "2": { 700 | "star_index": 1543691201, 701 | "get_star_ts": 1543691201, 702 | } 703 | }, 704 | "2": { 705 | "1": { 706 | "star_index": 1543735545, 707 | "get_star_ts": 1543735545, 708 | }, 709 | "2": { 710 | "star_index": 1543736319, 711 | "get_star_ts": 1543736319, 712 | } 713 | }, 714 | "3": { 715 | "1": { 716 | "star_index": 1543814258, 717 | "get_star_ts": 1543814258, 718 | }, 719 | "2": { 720 | "star_index": 1543815185, 721 | "get_star_ts": 1543815185, 722 | } 723 | }, 724 | "4": { 725 | "1": { 726 | "star_index": 1543903754, 727 | "get_star_ts": 1543903754, 728 | }, 729 | "2": { 730 | "star_index": 1543904161, 731 | "get_star_ts": 1543904161, 732 | } 733 | }, 734 | "5": { 735 | "1": { 736 | "star_index": 1543987099, 737 | "get_star_ts": 1543987099, 738 | }, 739 | "2": { 740 | "star_index": 1543988426, 741 | "get_star_ts": 1543988426, 742 | } 743 | }, 744 | "6": { 745 | "1": { 746 | "star_index": 1544077609, 747 | "get_star_ts": 1544077609, 748 | }, 749 | "2": { 750 | "star_index": 1544078081, 751 | "get_star_ts": 1544078081, 752 | } 753 | }, 754 | "7": { 755 | "1": { 756 | "star_index": 1544168700, 757 | "get_star_ts": 1544168700, 758 | }, 759 | "2": { 760 | "star_index": 1544170406, 761 | "get_star_ts": 1544170406, 762 | } 763 | } 764 | }, 765 | "local_score": 336, 766 | "global_score": 0, 767 | "last_star_ts": "1544170406" 768 | }, 769 | "100017": { 770 | "name": "l!m0nc3ll0", 771 | "stars": 0, 772 | "id": "100017", 773 | "last_star_ts": 0, 774 | "completion_day_level": {}, 775 | "global_score": 0, 776 | "local_score": 0 777 | } 778 | }, 779 | "owner_id": "190664", 780 | "event": "2018" 781 | }; 782 | }(window.aoc = window.aoc || {})); -------------------------------------------------------------------------------- /src/js/dummyData-empty.js: -------------------------------------------------------------------------------- 1 | (function (aoc) { 2 | aoc["dummyData"] = { 3 | "members": { 4 | "190664": { 5 | "name": "Jeroen Heijmans", 6 | "stars": 0, 7 | "id": "190664", 8 | "last_star_ts": "1544165499", 9 | "local_score": 0, 10 | "completion_day_level": { 11 | }, 12 | "global_score": 0 13 | }, 14 | }, 15 | "owner_id": "190664", 16 | "event": "2023" 17 | }; 18 | }(window.aoc = window.aoc || {})); -------------------------------------------------------------------------------- /src/js/dummyData-oneDay.js: -------------------------------------------------------------------------------- 1 | (function (aoc) { 2 | aoc["dummyData"] = { 3 | members: { 4 | 12345: { 5 | last_star_ts: 0, 6 | local_score: 0, 7 | completion_day_level: {}, 8 | stars: 0, 9 | name: "Senor Wontplaythisyear", 10 | global_score: 0, 11 | id: 12345, 12 | }, 13 | 23456: { 14 | name: "Felt Welkore", 15 | global_score: 0, 16 | id: 23456, 17 | stars: 2, 18 | last_star_ts: 1701407764, 19 | local_score: 81, 20 | completion_day_level: { 21 | 1: { 22 | 1: { 23 | star_index: 775, 24 | get_star_ts: 1701407311, 25 | }, 26 | 2: { 27 | get_star_ts: 1701407764, 28 | star_index: 4735, 29 | }, 30 | }, 31 | }, 32 | }, 33 | 34567: { 34 | name: "Sté der Bres", 35 | global_score: 0, 36 | id: 34567, 37 | stars: 0, 38 | local_score: 0, 39 | completion_day_level: {}, 40 | last_star_ts: 0, 41 | }, 42 | 45678: { 43 | local_score: 0, 44 | completion_day_level: {}, 45 | last_star_ts: 0, 46 | global_score: 0, 47 | name: "mcguffin", 48 | id: 45678, 49 | stars: 0, 50 | }, 51 | 56789: { 52 | completion_day_level: { 53 | 1: { 54 | 1: { 55 | star_index: 0, 56 | get_star_ts: 1701407237, 57 | }, 58 | 2: { 59 | star_index: 12083, 60 | get_star_ts: 1701409132, 61 | }, 62 | }, 63 | }, 64 | local_score: 81, 65 | last_star_ts: 1701409132, 66 | global_score: 0, 67 | id: 56789, 68 | name: "Jeroen Heijmans", 69 | stars: 2, 70 | }, 71 | 98765: { 72 | completion_day_level: { 73 | 1: { 74 | 1: { 75 | star_index: 58850, 76 | get_star_ts: 1701422979, 77 | }, 78 | }, 79 | }, 80 | local_score: 34, 81 | last_star_ts: 1701422979, 82 | name: "Martha G.", 83 | global_score: 0, 84 | id: 98765, 85 | stars: 1, 86 | }, 87 | 65432: { 88 | local_score: 58, 89 | completion_day_level: { 90 | 1: { 91 | 1: { 92 | get_star_ts: 1701444654, 93 | star_index: 138452, 94 | }, 95 | 2: { 96 | star_index: 142448, 97 | get_star_ts: 1701445648, 98 | }, 99 | }, 100 | }, 101 | last_star_ts: 1701445648, 102 | global_score: 0, 103 | id: 65432, 104 | name: "pittar93", 105 | stars: 2, 106 | }, 107 | 54321: { 108 | stars: 1, 109 | id: 54321, 110 | global_score: 0, 111 | name: "CapnCr00k", 112 | completion_day_level: { 113 | 1: { 114 | 1: { 115 | get_star_ts: 1701423684, 116 | star_index: 61614, 117 | }, 118 | }, 119 | }, 120 | local_score: 33, 121 | last_star_ts: 1701423684, 122 | }, 123 | 11223: { 124 | name: "Acrill West", 125 | global_score: 0, 126 | id: 11223, 127 | stars: 2, 128 | last_star_ts: 1701420716, 129 | local_score: 76, 130 | completion_day_level: { 131 | 1: { 132 | 1: { 133 | get_star_ts: 1701418276, 134 | star_index: 40795, 135 | }, 136 | 2: { 137 | get_star_ts: 1701420716, 138 | star_index: 49809, 139 | }, 140 | }, 141 | }, 142 | }, 143 | }, 144 | owner_id: 366773, 145 | event: "2023", 146 | }; 147 | })((window.aoc = window.aoc || {})); 148 | -------------------------------------------------------------------------------- /src/js/dummyData-regular.js: -------------------------------------------------------------------------------- 1 | (function (aoc) { 2 | aoc["dummyData"] = { 3 | members: { 4 | 714373: { 5 | completion_day_level: { 6 | 6: { 7 | 1: { get_star_ts: 1670310958, star_index: 1494771 }, 8 | 2: { star_index: 1494931, get_star_ts: 1670310989 }, 9 | }, 10 | 8: { 11 | 1: { star_index: 2003389, get_star_ts: 1670488973 }, 12 | 2: { star_index: 2011512, get_star_ts: 1670491519 }, 13 | }, 14 | 3: { 15 | 2: { star_index: 782676, get_star_ts: 1670097220 }, 16 | 1: { get_star_ts: 1670096641, star_index: 780948 }, 17 | }, 18 | 7: { 19 | 1: { star_index: 1804228, get_star_ts: 1670411785 }, 20 | 2: { star_index: 1804722, get_star_ts: 1670411983 }, 21 | }, 22 | 4: { 23 | 1: { star_index: 902784, get_star_ts: 1670141735 }, 24 | 2: { get_star_ts: 1670142372, star_index: 905265 }, 25 | }, 26 | 5: { 27 | 2: { star_index: 1181292, get_star_ts: 1670225898 }, 28 | 1: { star_index: 1179064, get_star_ts: 1670225348 }, 29 | }, 30 | 1: { 31 | 1: { star_index: 32054, get_star_ts: 1669878176 }, 32 | 2: { get_star_ts: 1669878261, star_index: 32319 }, 33 | }, 34 | 2: { 35 | 1: { get_star_ts: 1669968274, star_index: 331223 }, 36 | 2: { get_star_ts: 1669968938, star_index: 334697 }, 37 | }, 38 | }, 39 | last_star_ts: 1670491519, 40 | local_score: 493, 41 | name: "Drinker Trona", 42 | global_score: 0, 43 | stars: 16, 44 | id: 714373, 45 | }, 46 | 366773: { 47 | stars: 0, 48 | id: 366773, 49 | global_score: 0, 50 | local_score: 0, 51 | name: "Bulbil Elite", 52 | last_star_ts: 0, 53 | completion_day_level: {}, 54 | }, 55 | 463439: { 56 | global_score: 0, 57 | id: 463439, 58 | stars: 0, 59 | local_score: 0, 60 | name: "Piragua Mating", 61 | last_star_ts: 0, 62 | completion_day_level: {}, 63 | }, 64 | 380019: { 65 | last_star_ts: 1670099349, 66 | completion_day_level: { 67 | 3: { 68 | 1: { get_star_ts: 1670087062, star_index: 750201 }, 69 | 2: { get_star_ts: 1670099349, star_index: 789043 }, 70 | }, 71 | 2: { 72 | 1: { star_index: 532056, get_star_ts: 1670017133 }, 73 | 2: { get_star_ts: 1670018299, star_index: 535386 }, 74 | }, 75 | }, 76 | global_score: 0, 77 | id: 380019, 78 | stars: 4, 79 | name: "Jingle Ungainly", 80 | local_score: 100, 81 | }, 82 | 110810: { 83 | last_star_ts: 0, 84 | completion_day_level: {}, 85 | global_score: 0, 86 | stars: 0, 87 | id: 110810, 88 | name: "Chary", 89 | local_score: 0, 90 | }, 91 | 200603: { 92 | last_star_ts: 0, 93 | completion_day_level: {}, 94 | global_score: 0, 95 | stars: 0, 96 | id: 200603, 97 | name: "Mitis", 98 | local_score: 0, 99 | }, 100 | 190664: { 101 | local_score: 1640, 102 | name: "Jeroen Heijmans", 103 | global_score: 0, 104 | stars: 50, 105 | id: 190664, 106 | completion_day_level: { 107 | 9: { 108 | 2: { star_index: 2414870, get_star_ts: 1670672879 }, 109 | 1: { get_star_ts: 1670621873, star_index: 2315932 }, 110 | }, 111 | 14: { 112 | 1: { get_star_ts: 1671046558, star_index: 3046256 }, 113 | 2: { star_index: 3047840, get_star_ts: 1671047666 }, 114 | }, 115 | 17: { 116 | 1: { get_star_ts: 1672257774, star_index: 3865900 }, 117 | 2: { get_star_ts: 1672760598, star_index: 3955019 }, 118 | }, 119 | 19: { 120 | 1: { star_index: 3968267, get_star_ts: 1672850625 }, 121 | 2: { star_index: 3968336, get_star_ts: 1672850948 }, 122 | }, 123 | 12: { 124 | 1: { star_index: 2694313, get_star_ts: 1670823540 }, 125 | 2: { get_star_ts: 1670824068, star_index: 2695928 }, 126 | }, 127 | 16: { 128 | 2: { get_star_ts: 1672612195, star_index: 3933112 }, 129 | 1: { get_star_ts: 1672606657, star_index: 3931983 }, 130 | }, 131 | 15: { 132 | 1: { get_star_ts: 1671144316, star_index: 3166006 }, 133 | 2: { get_star_ts: 1671145791, star_index: 3167683 }, 134 | }, 135 | 11: { 136 | 1: { get_star_ts: 1670736814, star_index: 2527325 }, 137 | 2: { get_star_ts: 1672601258, star_index: 3930844 }, 138 | }, 139 | 4: { 140 | 2: { star_index: 911864, get_star_ts: 1670143982 }, 141 | 1: { star_index: 909586, get_star_ts: 1670143449 }, 142 | }, 143 | 25: { 144 | 2: { get_star_ts: 1672878018, star_index: 3972876 }, 145 | 1: { get_star_ts: 1672878014, star_index: 3972874 }, 146 | }, 147 | 20: { 148 | 1: { get_star_ts: 1672418224, star_index: 3902864 }, 149 | 2: { star_index: 3905112, get_star_ts: 1672426757 }, 150 | }, 151 | 23: { 152 | 1: { get_star_ts: 1672440528, star_index: 3909096 }, 153 | 2: { get_star_ts: 1672440778, star_index: 3909155 }, 154 | }, 155 | 6: { 156 | 2: { get_star_ts: 1670303916, star_index: 1453621 }, 157 | 1: { star_index: 1452332, get_star_ts: 1670303830 }, 158 | }, 159 | 21: { 160 | 1: { star_index: 3905431, get_star_ts: 1672427892 }, 161 | 2: { star_index: 3906427, get_star_ts: 1672431383 }, 162 | }, 163 | 13: { 164 | 1: { star_index: 2859609, get_star_ts: 1670919949 }, 165 | 2: { get_star_ts: 1670922010, star_index: 2863044 }, 166 | }, 167 | 8: { 168 | 2: { get_star_ts: 1670617790, star_index: 2307632 }, 169 | 1: { get_star_ts: 1670616318, star_index: 2304625 }, 170 | }, 171 | 24: { 172 | 2: { star_index: 3928915, get_star_ts: 1672591826 }, 173 | 1: { get_star_ts: 1672590950, star_index: 3928745 }, 174 | }, 175 | 3: { 176 | 2: { get_star_ts: 1670063200, star_index: 661299 }, 177 | 1: { star_index: 649209, get_star_ts: 1670060380 }, 178 | }, 179 | 7: { 180 | 1: { get_star_ts: 1670437064, star_index: 1874334 }, 181 | 2: { get_star_ts: 1670437569, star_index: 1875721 }, 182 | }, 183 | 22: { 184 | 1: { get_star_ts: 1672863257, star_index: 3970463 }, 185 | 2: { get_star_ts: 1672875390, star_index: 3972541 }, 186 | }, 187 | 2: { 188 | 1: { star_index: 265441, get_star_ts: 1669957737 }, 189 | 2: { star_index: 269280, get_star_ts: 1669957980 }, 190 | }, 191 | 5: { 192 | 1: { get_star_ts: 1670232569, star_index: 1210685 }, 193 | 2: { star_index: 1213196, get_star_ts: 1670233089 }, 194 | }, 195 | 18: { 196 | 2: { get_star_ts: 1672774070, star_index: 3957685 }, 197 | 1: { star_index: 3777515, get_star_ts: 1671986596 }, 198 | }, 199 | 1: { 200 | 2: { get_star_ts: 1669871718, star_index: 7773 }, 201 | 1: { star_index: 4779, get_star_ts: 1669871447 }, 202 | }, 203 | 10: { 204 | 1: { get_star_ts: 1670673956, star_index: 2417440 }, 205 | 2: { star_index: 2422390, get_star_ts: 1670675998 }, 206 | }, 207 | }, 208 | last_star_ts: 1672878018, 209 | }, 210 | 1587706: { 211 | id: 1587706, 212 | stars: 0, 213 | global_score: 0, 214 | name: "Engender", 215 | local_score: 0, 216 | last_star_ts: 0, 217 | completion_day_level: {}, 218 | }, 219 | 2739302: { 220 | last_star_ts: 0, 221 | completion_day_level: {}, 222 | stars: 0, 223 | id: 2739302, 224 | global_score: 0, 225 | local_score: 0, 226 | name: "Yahool", 227 | }, 228 | 367611: { 229 | completion_day_level: { 230 | 6: { 231 | 2: { star_index: 1619734, get_star_ts: 1670340518 }, 232 | 1: { get_star_ts: 1670340401, star_index: 1619301 }, 233 | }, 234 | 8: { 235 | 2: { get_star_ts: 1670595469, star_index: 2257897 }, 236 | 1: { get_star_ts: 1670592911, star_index: 2251314 }, 237 | }, 238 | 3: { 239 | 1: { star_index: 729383, get_star_ts: 1670081044 }, 240 | 2: { get_star_ts: 1670081666, star_index: 731533 }, 241 | }, 242 | 7: { 243 | 2: { get_star_ts: 1670435375, star_index: 1869843 }, 244 | 1: { get_star_ts: 1670433866, star_index: 1865760 }, 245 | }, 246 | 4: { 247 | 2: { get_star_ts: 1670143170, star_index: 908444 }, 248 | 1: { star_index: 906737, get_star_ts: 1670142736 }, 249 | }, 250 | 5: { 251 | 1: { get_star_ts: 1670272917, star_index: 1370350 }, 252 | 2: { get_star_ts: 1670273117, star_index: 1371119 }, 253 | }, 254 | 1: { 255 | 1: { star_index: 198184, get_star_ts: 1669924423 }, 256 | 2: { get_star_ts: 1669924723, star_index: 199244 }, 257 | }, 258 | 2: { 259 | 1: { star_index: 333206, get_star_ts: 1669968665 }, 260 | 2: { star_index: 350417, get_star_ts: 1669971621 }, 261 | }, 262 | }, 263 | last_star_ts: 1670595469, 264 | name: "Tented", 265 | local_score: 447, 266 | id: 367611, 267 | stars: 16, 268 | global_score: 0, 269 | }, 270 | 185974: { 271 | stars: 25, 272 | id: 185974, 273 | global_score: 0, 274 | local_score: 739, 275 | name: "Bourn", 276 | last_star_ts: 1672305447, 277 | completion_day_level: { 278 | 1: { 279 | 1: { get_star_ts: 1669884210, star_index: 53707 }, 280 | 2: { get_star_ts: 1669884433, star_index: 54678 }, 281 | }, 282 | 10: { 283 | 1: { get_star_ts: 1670684701, star_index: 2443186 }, 284 | 2: { get_star_ts: 1670685882, star_index: 2445970 }, 285 | }, 286 | 5: { 287 | 2: { get_star_ts: 1670234175, star_index: 1218325 }, 288 | 1: { get_star_ts: 1670233539, star_index: 1215326 }, 289 | }, 290 | 2: { 291 | 2: { get_star_ts: 1669975870, star_index: 374308 }, 292 | 1: { star_index: 370918, get_star_ts: 1669975198 }, 293 | }, 294 | 7: { 295 | 1: { star_index: 1840851, get_star_ts: 1670425298 }, 296 | 2: { get_star_ts: 1670425882, star_index: 1842539 }, 297 | }, 298 | 3: { 299 | 2: { star_index: 789283, get_star_ts: 1670099439 }, 300 | 1: { star_index: 786043, get_star_ts: 1670098346 }, 301 | }, 302 | 4: { 303 | 1: { get_star_ts: 1670145635, star_index: 919253 }, 304 | 2: { star_index: 920374, get_star_ts: 1670145873 }, 305 | }, 306 | 11: { 307 | 2: { star_index: 2715385, get_star_ts: 1670833772 }, 308 | 1: { get_star_ts: 1670789061, star_index: 2644616 }, 309 | }, 310 | 8: { 311 | 2: { star_index: 2045668, get_star_ts: 1670503618 }, 312 | 1: { star_index: 2013045, get_star_ts: 1670491992 }, 313 | }, 314 | 15: { 1: { star_index: 3196679, get_star_ts: 1671187620 } }, 315 | 21: { 316 | 1: { get_star_ts: 1672220406, star_index: 3853343 }, 317 | 2: { star_index: 3876506, get_star_ts: 1672305447 }, 318 | }, 319 | 9: { 320 | 2: { star_index: 2270705, get_star_ts: 1670600473 }, 321 | 1: { get_star_ts: 1670581531, star_index: 2224548 }, 322 | }, 323 | 6: { 324 | 2: { star_index: 1527446, get_star_ts: 1670317054 }, 325 | 1: { get_star_ts: 1670316540, star_index: 1524693 }, 326 | }, 327 | }, 328 | }, 329 | 124079: { 330 | last_star_ts: 0, 331 | completion_day_level: {}, 332 | stars: 0, 333 | id: 124079, 334 | global_score: 0, 335 | local_score: 0, 336 | name: "Marram", 337 | }, 338 | 2557628: { 339 | completion_day_level: { 340 | 8: { 1: { star_index: 2596931, get_star_ts: 1670768201 } }, 341 | 6: { 342 | 2: { star_index: 1908494, get_star_ts: 1670448778 }, 343 | 1: { star_index: 1906839, get_star_ts: 1670448234 }, 344 | }, 345 | 2: { 346 | 1: { get_star_ts: 1669997645, star_index: 467490 }, 347 | 2: { star_index: 471312, get_star_ts: 1669998570 }, 348 | }, 349 | 5: { 350 | 2: { star_index: 1813208, get_star_ts: 1670415270 }, 351 | 1: { get_star_ts: 1670415201, star_index: 1813026 }, 352 | }, 353 | 1: { 354 | 2: { get_star_ts: 1670188136, star_index: 1085307 }, 355 | 1: { star_index: 1084183, get_star_ts: 1670187792 }, 356 | }, 357 | 4: { 358 | 1: { star_index: 1048111, get_star_ts: 1670176894 }, 359 | 2: { star_index: 1052902, get_star_ts: 1670178304 }, 360 | }, 361 | 3: { 362 | 2: { star_index: 1080706, get_star_ts: 1670186732 }, 363 | 1: { star_index: 1078595, get_star_ts: 1670186094 }, 364 | }, 365 | 7: { 366 | 1: { get_star_ts: 1670535602, star_index: 2132350 }, 367 | 2: { star_index: 2568536, get_star_ts: 1670755711 }, 368 | }, 369 | }, 370 | last_star_ts: 1670768201, 371 | name: "Upright", 372 | local_score: 361, 373 | global_score: 0, 374 | stars: 15, 375 | id: 2557628, 376 | }, 377 | 195829: { 378 | completion_day_level: {}, 379 | last_star_ts: 0, 380 | local_score: 0, 381 | name: "Tortoise", 382 | id: 195829, 383 | stars: 0, 384 | global_score: 0, 385 | }, 386 | 608033: { 387 | global_score: 0, 388 | id: 608033, 389 | stars: 23, 390 | local_score: 718, 391 | name: "Axes", 392 | last_star_ts: 1671007229, 393 | completion_day_level: { 394 | 14: { 395 | 2: { get_star_ts: 1671007229, star_index: 2987611 }, 396 | 1: { star_index: 2986640, get_star_ts: 1671006592 }, 397 | }, 398 | 6: { 399 | 2: { star_index: 1488337, get_star_ts: 1670309631 }, 400 | 1: { get_star_ts: 1670309594, star_index: 1488155 }, 401 | }, 402 | 9: { 403 | 1: { get_star_ts: 1670615070, star_index: 2302104 }, 404 | 2: { get_star_ts: 1670616149, star_index: 2304273 }, 405 | }, 406 | 8: { 407 | 1: { get_star_ts: 1670477206, star_index: 1959062 }, 408 | 2: { star_index: 2310231, get_star_ts: 1670619049 }, 409 | }, 410 | 11: { 1: { star_index: 2579781, get_star_ts: 1670760623 } }, 411 | 3: { 412 | 1: { star_index: 624571, get_star_ts: 1670053817 }, 413 | 2: { star_index: 626108, get_star_ts: 1670054274 }, 414 | }, 415 | 7: { 416 | 2: { star_index: 1758760, get_star_ts: 1670396365 }, 417 | 1: { star_index: 1757481, get_star_ts: 1670395948 }, 418 | }, 419 | 4: { 420 | 2: { star_index: 936486, get_star_ts: 1670149217 }, 421 | 1: { get_star_ts: 1670147839, star_index: 929786 }, 422 | }, 423 | 5: { 424 | 1: { star_index: 1357531, get_star_ts: 1670269655 }, 425 | 2: { get_star_ts: 1670270104, star_index: 1359257 }, 426 | }, 427 | 1: { 428 | 2: { get_star_ts: 1669875956, star_index: 25252 }, 429 | 1: { star_index: 24160, get_star_ts: 1669875599 }, 430 | }, 431 | 10: { 432 | 1: { star_index: 2405951, get_star_ts: 1670669427 }, 433 | 2: { star_index: 2414950, get_star_ts: 1670672913 }, 434 | }, 435 | 2: { 436 | 2: { star_index: 471166, get_star_ts: 1669998542 }, 437 | 1: { get_star_ts: 1669961404, star_index: 298781 }, 438 | }, 439 | }, 440 | }, 441 | 1057461: { 442 | last_star_ts: 1672149247, 443 | completion_day_level: { 444 | 6: { 445 | 2: { get_star_ts: 1672149247, star_index: 3832942 }, 446 | 1: { get_star_ts: 1672148708, star_index: 3832770 }, 447 | }, 448 | 4: { 449 | 2: { get_star_ts: 1670666330, star_index: 2397689 }, 450 | 1: { get_star_ts: 1670612681, star_index: 2297421 }, 451 | }, 452 | 3: { 453 | 2: { star_index: 2234737, get_star_ts: 1670585734 }, 454 | 1: { star_index: 2227454, get_star_ts: 1670582695 }, 455 | }, 456 | 2: { 457 | 1: { get_star_ts: 1669991838, star_index: 441851 }, 458 | 2: { get_star_ts: 1669995387, star_index: 457730 }, 459 | }, 460 | 5: { 461 | 1: { get_star_ts: 1670760927, star_index: 2580465 }, 462 | 2: { get_star_ts: 1670761443, star_index: 2581594 }, 463 | }, 464 | 1: { 465 | 1: { star_index: 209299, get_star_ts: 1669927788 }, 466 | 2: { get_star_ts: 1669930061, star_index: 216492 }, 467 | }, 468 | }, 469 | global_score: 0, 470 | id: 1057461, 471 | stars: 12, 472 | local_score: 267, 473 | name: "Miscount", 474 | }, 475 | 207647: { 476 | name: "Whoa", 477 | local_score: 1752, 478 | id: 207647, 479 | stars: 48, 480 | global_score: 30, 481 | completion_day_level: { 482 | 23: { 483 | 2: { get_star_ts: 1671775380, star_index: 3652460 }, 484 | 1: { star_index: 3652289, get_star_ts: 1671775268 }, 485 | }, 486 | 20: { 487 | 1: { get_star_ts: 1671528215, star_index: 3471852 }, 488 | 2: { star_index: 3481535, get_star_ts: 1671539916 }, 489 | }, 490 | 25: { 1: { star_index: 3773209, get_star_ts: 1671978652 } }, 491 | 4: { 492 | 2: { get_star_ts: 1670130491, star_index: 847152 }, 493 | 1: { get_star_ts: 1670130230, star_index: 842787 }, 494 | }, 495 | 11: { 496 | 1: { get_star_ts: 1670736272, star_index: 2525257 }, 497 | 2: { star_index: 2526660, get_star_ts: 1670736664 }, 498 | }, 499 | 16: { 1: { star_index: 3187635, get_star_ts: 1671176904 } }, 500 | 15: { 501 | 2: { get_star_ts: 1671084653, star_index: 3086678 }, 502 | 1: { star_index: 3080939, get_star_ts: 1671082171 }, 503 | }, 504 | 12: { 505 | 1: { get_star_ts: 1670824142, star_index: 2696171 }, 506 | 2: { get_star_ts: 1670825248, star_index: 2699159 }, 507 | }, 508 | 19: { 509 | 1: { star_index: 3397503, get_star_ts: 1671428687 }, 510 | 2: { star_index: 3397905, get_star_ts: 1671429406 }, 511 | }, 512 | 14: { 513 | 2: { star_index: 2963676, get_star_ts: 1670995512 }, 514 | 1: { star_index: 2962375, get_star_ts: 1670995139 }, 515 | }, 516 | 17: { 517 | 1: { get_star_ts: 1671267422, star_index: 3262825 }, 518 | 2: { get_star_ts: 1671278281, star_index: 3271525 }, 519 | }, 520 | 9: { 521 | 2: { star_index: 2174664, get_star_ts: 1670563950 }, 522 | 1: { get_star_ts: 1670562573, star_index: 2168023 }, 523 | }, 524 | 18: { 525 | 1: { get_star_ts: 1671340142, star_index: 3321179 }, 526 | 2: { star_index: 3323846, get_star_ts: 1671341101 }, 527 | }, 528 | 10: { 529 | 2: { star_index: 2358106, get_star_ts: 1670650642 }, 530 | 1: { get_star_ts: 1670649372, star_index: 2349714 }, 531 | }, 532 | 1: { 533 | 2: { star_index: 2577, get_star_ts: 1669871290 }, 534 | 1: { star_index: 0, get_star_ts: 1669871148 }, 535 | }, 536 | 5: { 537 | 2: { star_index: 1135481, get_star_ts: 1670217303 }, 538 | 1: { star_index: 1134343, get_star_ts: 1670217139 }, 539 | }, 540 | 2: { 541 | 1: { get_star_ts: 1669958830, star_index: 281309 }, 542 | 2: { star_index: 284952, get_star_ts: 1669959193 }, 543 | }, 544 | 22: { 545 | 2: { star_index: 3600107, get_star_ts: 1671693884 }, 546 | 1: { star_index: 3595657, get_star_ts: 1671688346 }, 547 | }, 548 | 7: { 549 | 2: { star_index: 1755466, get_star_ts: 1670395326 }, 550 | 1: { get_star_ts: 1670392973, star_index: 1746497 }, 551 | }, 552 | 3: { 553 | 2: { get_star_ts: 1670044759, star_index: 583410 }, 554 | 1: { get_star_ts: 1670044033, star_index: 574126 }, 555 | }, 556 | 24: { 557 | 2: { get_star_ts: 1671865062, star_index: 3711495 }, 558 | 1: { star_index: 3709647, get_star_ts: 1671862640 }, 559 | }, 560 | 8: { 561 | 2: { get_star_ts: 1670477057, star_index: 1958038 }, 562 | 1: { get_star_ts: 1670476513, star_index: 1954402 }, 563 | }, 564 | 13: { 565 | 2: { star_index: 2913795, get_star_ts: 1670953461 }, 566 | 1: { star_index: 2912589, get_star_ts: 1670952706 }, 567 | }, 568 | 21: { 569 | 2: { star_index: 3530581, get_star_ts: 1671602627 }, 570 | 1: { star_index: 3523977, get_star_ts: 1671599265 }, 571 | }, 572 | 6: { 573 | 1: { star_index: 1434413, get_star_ts: 1670303027 }, 574 | 2: { get_star_ts: 1670303063, star_index: 1435253 }, 575 | }, 576 | }, 577 | last_star_ts: 1671978652, 578 | }, 579 | 807244: { 580 | stars: 0, 581 | id: 807244, 582 | global_score: 0, 583 | name: "Sumach", 584 | local_score: 0, 585 | last_star_ts: 0, 586 | completion_day_level: {}, 587 | }, 588 | 383497: { 589 | global_score: 0, 590 | stars: 29, 591 | id: 383497, 592 | name: "Combinedcyclamen", 593 | local_score: 865, 594 | last_star_ts: 1671442995, 595 | completion_day_level: { 596 | 9: { 597 | 1: { star_index: 2221772, get_star_ts: 1670580435 }, 598 | 2: { star_index: 2222705, get_star_ts: 1670580807 }, 599 | }, 600 | 6: { 601 | 2: { star_index: 1521065, get_star_ts: 1670315875 }, 602 | 1: { star_index: 1520692, get_star_ts: 1670315807 }, 603 | }, 604 | 14: { 605 | 2: { star_index: 3044417, get_star_ts: 1671045210 }, 606 | 1: { get_star_ts: 1671044027, star_index: 3042702 }, 607 | }, 608 | 13: { 609 | 1: { get_star_ts: 1671036297, star_index: 3031559 }, 610 | 2: { star_index: 3033659, get_star_ts: 1671037719 }, 611 | }, 612 | 12: { 613 | 1: { star_index: 2946236, get_star_ts: 1670973507 }, 614 | 2: { get_star_ts: 1670974238, star_index: 2947083 }, 615 | }, 616 | 11: { 617 | 2: { get_star_ts: 1670763374, star_index: 2585972 }, 618 | 1: { star_index: 2583753, get_star_ts: 1670762407 }, 619 | }, 620 | 8: { 621 | 2: { get_star_ts: 1670493905, star_index: 2019124 }, 622 | 1: { get_star_ts: 1670492579, star_index: 2014944 }, 623 | }, 624 | 15: { 1: { get_star_ts: 1671442995, star_index: 3407172 } }, 625 | 4: { 626 | 2: { get_star_ts: 1670262982, star_index: 1332614 }, 627 | 1: { get_star_ts: 1670262789, star_index: 1331940 }, 628 | }, 629 | 7: { 630 | 1: { star_index: 1784114, get_star_ts: 1670405044 }, 631 | 2: { star_index: 1788670, get_star_ts: 1670406529 }, 632 | }, 633 | 3: { 634 | 1: { star_index: 1255123, get_star_ts: 1670243262 }, 635 | 2: { get_star_ts: 1670243647, star_index: 1256534 }, 636 | }, 637 | 2: { 638 | 2: { star_index: 325905, get_star_ts: 1669967276 }, 639 | 1: { star_index: 323589, get_star_ts: 1669966837 }, 640 | }, 641 | 10: { 642 | 2: { star_index: 2403200, get_star_ts: 1670668415 }, 643 | 1: { get_star_ts: 1670665513, star_index: 2395558 }, 644 | }, 645 | 1: { 646 | 2: { star_index: 62141, get_star_ts: 1669886183 }, 647 | 1: { star_index: 59614, get_star_ts: 1669885584 }, 648 | }, 649 | 5: { 650 | 2: { get_star_ts: 1670271189, star_index: 1363484 }, 651 | 1: { star_index: 1361201, get_star_ts: 1670270593 }, 652 | }, 653 | }, 654 | }, 655 | 385651: { 656 | global_score: 0, 657 | stars: 15, 658 | id: 385651, 659 | name: "Agleydariole", 660 | local_score: 380, 661 | last_star_ts: 1670787487, 662 | completion_day_level: { 663 | 4: { 664 | 1: { star_index: 955682, get_star_ts: 1670153200 }, 665 | 2: { star_index: 955846, get_star_ts: 1670153231 }, 666 | }, 667 | 7: { 668 | 1: { star_index: 2255984, get_star_ts: 1670594694 }, 669 | 2: { get_star_ts: 1670595472, star_index: 2257904 }, 670 | }, 671 | 3: { 672 | 1: { star_index: 730395, get_star_ts: 1670081327 }, 673 | 2: { star_index: 731493, get_star_ts: 1670081654 }, 674 | }, 675 | 2: { 676 | 1: { star_index: 688587, get_star_ts: 1670069942 }, 677 | 2: { star_index: 689476, get_star_ts: 1670070170 }, 678 | }, 679 | 10: { 1: { star_index: 2640991, get_star_ts: 1670787487 } }, 680 | 1: { 681 | 2: { get_star_ts: 1670068204, star_index: 681935 }, 682 | 1: { star_index: 679092, get_star_ts: 1670067484 }, 683 | }, 684 | 5: { 685 | 1: { get_star_ts: 1670235782, star_index: 1225626 }, 686 | 2: { star_index: 1226531, get_star_ts: 1670235984 }, 687 | }, 688 | 6: { 689 | 2: { get_star_ts: 1670506081, star_index: 2052224 }, 690 | 1: { star_index: 2051988, get_star_ts: 1670505991 }, 691 | }, 692 | }, 693 | }, 694 | 2448342: { 695 | last_star_ts: 1671353059, 696 | completion_day_level: { 697 | 8: { 698 | 2: { star_index: 1982743, get_star_ts: 1670482335 }, 699 | 1: { star_index: 1979502, get_star_ts: 1670481357 }, 700 | }, 701 | 13: { 702 | 2: { star_index: 3009471, get_star_ts: 1671021847 }, 703 | 1: { star_index: 3008793, get_star_ts: 1671021345 }, 704 | }, 705 | 6: { 706 | 1: { get_star_ts: 1670306790, star_index: 1475051 }, 707 | 2: { star_index: 1475499, get_star_ts: 1670306886 }, 708 | }, 709 | 10: { 710 | 2: { star_index: 2377638, get_star_ts: 1670657689 }, 711 | 1: { get_star_ts: 1670656471, star_index: 2375163 }, 712 | }, 713 | 1: { 714 | 2: { get_star_ts: 1669992709, star_index: 445892 }, 715 | 1: { get_star_ts: 1669992477, star_index: 444841 }, 716 | }, 717 | 18: { 718 | 1: { star_index: 3333855, get_star_ts: 1671350780 }, 719 | 2: { get_star_ts: 1671353059, star_index: 3335921 }, 720 | }, 721 | 5: { 722 | 1: { get_star_ts: 1670223421, star_index: 1171897 }, 723 | 2: { star_index: 1172667, get_star_ts: 1670223633 }, 724 | }, 725 | 2: { 726 | 1: { star_index: 452027, get_star_ts: 1669994078 }, 727 | 2: { get_star_ts: 1669994489, star_index: 453781 }, 728 | }, 729 | 7: { 730 | 2: { get_star_ts: 1670393621, star_index: 1749172 }, 731 | 1: { star_index: 1747830, get_star_ts: 1670393298 }, 732 | }, 733 | 3: { 734 | 1: { star_index: 631157, get_star_ts: 1670055741 }, 735 | 2: { get_star_ts: 1670056998, star_index: 635876 }, 736 | }, 737 | 11: { 738 | 1: { star_index: 2558003, get_star_ts: 1670750822 }, 739 | 2: { star_index: 2562844, get_star_ts: 1670753201 }, 740 | }, 741 | 15: { 742 | 2: { get_star_ts: 1671099668, star_index: 3107356 }, 743 | 1: { star_index: 3095338, get_star_ts: 1671090456 }, 744 | }, 745 | 16: { 746 | 1: { get_star_ts: 1671265097, star_index: 3261206 }, 747 | 2: { get_star_ts: 1671294352, star_index: 3286218 }, 748 | }, 749 | 12: { 750 | 1: { get_star_ts: 1670869263, star_index: 2781674 }, 751 | 2: { star_index: 2782688, get_star_ts: 1670869807 }, 752 | }, 753 | 14: { 754 | 2: { star_index: 3028495, get_star_ts: 1671034400 }, 755 | 1: { star_index: 3027929, get_star_ts: 1671034029 }, 756 | }, 757 | 17: { 758 | 1: { get_star_ts: 1671301832, star_index: 3293440 }, 759 | 2: { get_star_ts: 1671348398, star_index: 3331859 }, 760 | }, 761 | 9: { 762 | 1: { star_index: 2169239, get_star_ts: 1670562935 }, 763 | 2: { get_star_ts: 1670573193, star_index: 2202941 }, 764 | }, 765 | 4: { 766 | 2: { star_index: 924459, get_star_ts: 1670146757 }, 767 | 1: { get_star_ts: 1670146510, star_index: 923281 }, 768 | }, 769 | }, 770 | id: 2448342, 771 | stars: 36, 772 | global_score: 0, 773 | name: "ComradeEmeritusTwentytwelve", 774 | local_score: 1191, 775 | }, 776 | 2265117: { 777 | local_score: 383, 778 | name: "Gravelhothouse", 779 | global_score: 0, 780 | stars: 17, 781 | id: 2265117, 782 | completion_day_level: { 783 | 5: { 784 | 2: { get_star_ts: 1670454130, star_index: 1923181 }, 785 | 1: { get_star_ts: 1670452218, star_index: 1918416 }, 786 | }, 787 | 1: { 788 | 2: { star_index: 1641520, get_star_ts: 1670346823 }, 789 | 1: { get_star_ts: 1670345122, star_index: 1635789 }, 790 | }, 791 | 2: { 792 | 2: { star_index: 1677278, get_star_ts: 1670358179 }, 793 | 1: { get_star_ts: 1670356324, star_index: 1671323 }, 794 | }, 795 | 3: { 796 | 1: { get_star_ts: 1670361175, star_index: 1686802 }, 797 | 2: { get_star_ts: 1670362064, star_index: 1689707 }, 798 | }, 799 | 7: { 800 | 2: { get_star_ts: 1672323251, star_index: 3880893 }, 801 | 1: { star_index: 3880460, get_star_ts: 1672321666 }, 802 | }, 803 | 4: { 804 | 1: { get_star_ts: 1670431582, star_index: 1859175 }, 805 | 2: { star_index: 1859470, get_star_ts: 1670431685 }, 806 | }, 807 | 8: { 808 | 1: { star_index: 3624695, get_star_ts: 1671728953 }, 809 | 2: { get_star_ts: 1671732179, star_index: 3627066 }, 810 | }, 811 | 6: { 812 | 2: { star_index: 1985153, get_star_ts: 1670483092 }, 813 | 1: { get_star_ts: 1670483024, star_index: 1984925 }, 814 | }, 815 | 9: { 1: { get_star_ts: 1672927768, star_index: 3977605 } }, 816 | }, 817 | last_star_ts: 1672927768, 818 | }, 819 | 1574079: { 820 | stars: 0, 821 | id: 1574079, 822 | global_score: 0, 823 | name: "Myriad venosity", 824 | local_score: 0, 825 | last_star_ts: 0, 826 | completion_day_level: {}, 827 | }, 828 | 383432: { 829 | last_star_ts: 0, 830 | completion_day_level: {}, 831 | stars: 0, 832 | id: 383432, 833 | global_score: 0, 834 | name: "niter", 835 | local_score: 0, 836 | }, 837 | 980565: { 838 | local_score: 1499, 839 | name: "hogweed", 840 | global_score: 0, 841 | id: 980565, 842 | stars: 43, 843 | completion_day_level: { 844 | 12: { 845 | 1: { star_index: 2713665, get_star_ts: 1670832840 }, 846 | 2: { get_star_ts: 1670923089, star_index: 2864802 }, 847 | }, 848 | 15: { 849 | 2: { star_index: 3145336, get_star_ts: 1671129147 }, 850 | 1: { star_index: 3093661, get_star_ts: 1671089133 }, 851 | }, 852 | 16: { 1: { get_star_ts: 1672313096, star_index: 3878216 } }, 853 | 11: { 854 | 1: { get_star_ts: 1670740888, star_index: 2540242 }, 855 | 2: { star_index: 2543309, get_star_ts: 1670742431 }, 856 | }, 857 | 9: { 858 | 2: { star_index: 2189416, get_star_ts: 1670567806 }, 859 | 1: { star_index: 2188044, get_star_ts: 1670567321 }, 860 | }, 861 | 17: { 862 | 2: { get_star_ts: 1671283244, star_index: 3275792 }, 863 | 1: { get_star_ts: 1671262714, star_index: 3259577 }, 864 | }, 865 | 14: { 866 | 1: { star_index: 2975088, get_star_ts: 1670999435 }, 867 | 2: { star_index: 2975578, get_star_ts: 1670999693 }, 868 | }, 869 | 25: { 1: { star_index: 3801663, get_star_ts: 1672052528 } }, 870 | 20: { 871 | 2: { get_star_ts: 1671531028, star_index: 3474200 }, 872 | 1: { get_star_ts: 1671525934, star_index: 3469987 }, 873 | }, 874 | 23: { 875 | 2: { get_star_ts: 1671793067, star_index: 3666364 }, 876 | 1: { get_star_ts: 1671792967, star_index: 3666296 }, 877 | }, 878 | 4: { 879 | 2: { get_star_ts: 1670133163, star_index: 872703 }, 880 | 1: { get_star_ts: 1670132831, star_index: 870953 }, 881 | }, 882 | 13: { 883 | 1: { star_index: 2849751, get_star_ts: 1670914096 }, 884 | 2: { get_star_ts: 1670914665, star_index: 2850755 }, 885 | }, 886 | 8: { 887 | 1: { get_star_ts: 1670479174, star_index: 1970644 }, 888 | 2: { get_star_ts: 1670480589, star_index: 1976717 }, 889 | }, 890 | 6: { 891 | 2: { star_index: 1502676, get_star_ts: 1670312449 }, 892 | 1: { get_star_ts: 1670312323, star_index: 1501969 }, 893 | }, 894 | 21: { 895 | 1: { star_index: 3538749, get_star_ts: 1671610960 }, 896 | 2: { get_star_ts: 1671626914, star_index: 3553419 }, 897 | }, 898 | 22: { 1: { get_star_ts: 1671696925, star_index: 3602034 } }, 899 | 2: { 900 | 1: { get_star_ts: 1669962432, star_index: 303514 }, 901 | 2: { get_star_ts: 1669962738, star_index: 304865 }, 902 | }, 903 | 5: { 904 | 1: { get_star_ts: 1670224948, star_index: 1177535 }, 905 | 2: { star_index: 1178545, get_star_ts: 1670225216 }, 906 | }, 907 | 10: { 908 | 2: { star_index: 2367304, get_star_ts: 1670653003 }, 909 | 1: { get_star_ts: 1670651315, star_index: 2361461 }, 910 | }, 911 | 18: { 912 | 2: { star_index: 3334922, get_star_ts: 1671352025 }, 913 | 1: { get_star_ts: 1671349660, star_index: 3332899 }, 914 | }, 915 | 1: { 916 | 1: { get_star_ts: 1669877218, star_index: 29024 }, 917 | 2: { get_star_ts: 1669877278, star_index: 29203 }, 918 | }, 919 | 3: { 920 | 2: { star_index: 607582, get_star_ts: 1670048688 }, 921 | 1: { star_index: 604979, get_star_ts: 1670048017 }, 922 | }, 923 | 7: { 924 | 1: { star_index: 1741677, get_star_ts: 1670391945 }, 925 | 2: { get_star_ts: 1670392305, star_index: 1743413 }, 926 | }, 927 | }, 928 | last_star_ts: 1672313096, 929 | }, 930 | 43661: { 931 | id: 43661, 932 | stars: 0, 933 | global_score: 0, 934 | name: "cutter", 935 | local_score: 0, 936 | last_star_ts: 0, 937 | completion_day_level: {}, 938 | }, 939 | 633522: { 940 | completion_day_level: { 941 | 25: { 1: { star_index: 3866668, get_star_ts: 1672259809 } }, 942 | 4: { 943 | 2: { get_star_ts: 1670140200, star_index: 897167 }, 944 | 1: { get_star_ts: 1670139899, star_index: 896140 }, 945 | }, 946 | 12: { 947 | 1: { get_star_ts: 1670871561, star_index: 2786063 }, 948 | 2: { get_star_ts: 1670872281, star_index: 2787462 }, 949 | }, 950 | 15: { 951 | 2: { star_index: 3143259, get_star_ts: 1671127536 }, 952 | 1: { get_star_ts: 1671113984, star_index: 3125297 }, 953 | }, 954 | 11: { 955 | 1: { star_index: 2561020, get_star_ts: 1670752327 }, 956 | 2: { get_star_ts: 1670776459, star_index: 2616137 }, 957 | }, 958 | 9: { 959 | 1: { get_star_ts: 1670573542, star_index: 2203795 }, 960 | 2: { star_index: 2255322, get_star_ts: 1670594426 }, 961 | }, 962 | 17: { 1: { star_index: 3440218, get_star_ts: 1671483168 } }, 963 | 14: { 964 | 1: { get_star_ts: 1671046660, star_index: 3046401 }, 965 | 2: { get_star_ts: 1671047255, star_index: 3047269 }, 966 | }, 967 | 2: { 968 | 2: { get_star_ts: 1669962128, star_index: 302102 }, 969 | 1: { get_star_ts: 1669961791, star_index: 300554 }, 970 | }, 971 | 5: { 972 | 1: { get_star_ts: 1670226174, star_index: 1182456 }, 973 | 2: { star_index: 1183158, get_star_ts: 1670226348 }, 974 | }, 975 | 18: { 1: { get_star_ts: 1671474751, star_index: 3432738 } }, 976 | 1: { 977 | 1: { get_star_ts: 1669875629, star_index: 24247 }, 978 | 2: { star_index: 25183, get_star_ts: 1669875934 }, 979 | }, 980 | 10: { 981 | 2: { star_index: 2474375, get_star_ts: 1670698778 }, 982 | 1: { get_star_ts: 1670694700, star_index: 2465901 }, 983 | }, 984 | 3: { 985 | 1: { star_index: 618808, get_star_ts: 1670052082 }, 986 | 2: { get_star_ts: 1670052583, star_index: 620456 }, 987 | }, 988 | 7: { 989 | 2: { get_star_ts: 1670399958, star_index: 1769221 }, 990 | 1: { get_star_ts: 1670399754, star_index: 1768635 }, 991 | }, 992 | 13: { 993 | 2: { get_star_ts: 1670918466, star_index: 2857131 }, 994 | 1: { get_star_ts: 1670917886, star_index: 2856139 }, 995 | }, 996 | 8: { 997 | 2: { star_index: 2028012, get_star_ts: 1670496888 }, 998 | 1: { get_star_ts: 1670495390, star_index: 2023677 }, 999 | }, 1000 | 6: { 1001 | 1: { star_index: 1488566, get_star_ts: 1670309676 }, 1002 | 2: { star_index: 1490054, get_star_ts: 1670309982 }, 1003 | }, 1004 | 21: { 1005 | 2: { star_index: 3607941, get_star_ts: 1671705659 }, 1006 | 1: { star_index: 3536451, get_star_ts: 1671608479 }, 1007 | }, 1008 | }, 1009 | last_star_ts: 1672259809, 1010 | name: "venation", 1011 | local_score: 1151, 1012 | stars: 35, 1013 | id: 633522, 1014 | global_score: 0, 1015 | }, 1016 | 109856: { 1017 | last_star_ts: 0, 1018 | completion_day_level: {}, 1019 | id: 109856, 1020 | stars: 0, 1021 | global_score: 0, 1022 | name: "closed", 1023 | local_score: 0, 1024 | }, 1025 | 182215: { 1026 | global_score: 0, 1027 | id: 182215, 1028 | stars: 0, 1029 | name: "tigerish", 1030 | local_score: 0, 1031 | last_star_ts: 0, 1032 | completion_day_level: {}, 1033 | }, 1034 | 1598831: { 1035 | id: 1598831, 1036 | stars: 0, 1037 | global_score: 0, 1038 | name: "hinder", 1039 | local_score: 0, 1040 | last_star_ts: 0, 1041 | completion_day_level: {}, 1042 | }, 1043 | 2475621: { 1044 | local_score: 288, 1045 | name: "daylong", 1046 | stars: 13, 1047 | id: 2475621, 1048 | global_score: 0, 1049 | completion_day_level: { 1050 | 3: { 1051 | 1: { star_index: 1767878, get_star_ts: 1670399495 }, 1052 | 2: { star_index: 1773847, get_star_ts: 1670401579 }, 1053 | }, 1054 | 4: { 1055 | 1: { get_star_ts: 1670769886, star_index: 2600844 }, 1056 | 2: { get_star_ts: 1670770147, star_index: 2601430 }, 1057 | }, 1058 | 6: { 1059 | 1: { star_index: 2649240, get_star_ts: 1670791036 }, 1060 | 2: { star_index: 2649540, get_star_ts: 1670791155 }, 1061 | }, 1062 | 1: { 1063 | 1: { star_index: 196433, get_star_ts: 1669923923 }, 1064 | 2: { star_index: 205449, get_star_ts: 1669926595 }, 1065 | }, 1066 | 5: { 1067 | 1: { get_star_ts: 1670775654, star_index: 2614244 }, 1068 | 2: { get_star_ts: 1670775825, star_index: 2614662 }, 1069 | }, 1070 | 8: { 1: { get_star_ts: 1671135798, star_index: 3154199 } }, 1071 | 2: { 1072 | 2: { get_star_ts: 1670340681, star_index: 1620339 }, 1073 | 1: { get_star_ts: 1669996224, star_index: 461454 }, 1074 | }, 1075 | }, 1076 | last_star_ts: 1671135798, 1077 | }, 1078 | 357021: { 1079 | completion_day_level: { 1080 | 2: { 1081 | 1: { star_index: 2057767, get_star_ts: 1670508012 }, 1082 | 2: { get_star_ts: 1670508699, star_index: 2059680 }, 1083 | }, 1084 | 5: { 1085 | 1: { get_star_ts: 1670879798, star_index: 2802823 }, 1086 | 2: { star_index: 2805688, get_star_ts: 1670881227 }, 1087 | }, 1088 | 1: { 1089 | 1: { get_star_ts: 1670505310, star_index: 2050190 }, 1090 | 2: { star_index: 2050989, get_star_ts: 1670505624 }, 1091 | }, 1092 | 6: { 1093 | 1: { star_index: 3105435, get_star_ts: 1671098161 }, 1094 | 2: { star_index: 3105505, get_star_ts: 1671098222 }, 1095 | }, 1096 | 4: { 1097 | 1: { get_star_ts: 1670795041, star_index: 2658234 }, 1098 | 2: { star_index: 2661666, get_star_ts: 1670796624 }, 1099 | }, 1100 | 3: { 1101 | 2: { get_star_ts: 1670792547, star_index: 2652815 }, 1102 | 1: { star_index: 2651523, get_star_ts: 1670791989 }, 1103 | }, 1104 | 7: { 1105 | 2: { get_star_ts: 1672355948, star_index: 3891403 }, 1106 | 1: { star_index: 3855187, get_star_ts: 1672227070 }, 1107 | }, 1108 | }, 1109 | last_star_ts: 1672355948, 1110 | local_score: 278, 1111 | name: "surety", 1112 | global_score: 0, 1113 | stars: 14, 1114 | id: 357021, 1115 | }, 1116 | 117458: { 1117 | local_score: 1722, 1118 | name: "speckle", 1119 | global_score: 0, 1120 | id: 117458, 1121 | stars: 50, 1122 | completion_day_level: { 1123 | 23: { 1124 | 2: { get_star_ts: 1671911758, star_index: 3737510 }, 1125 | 1: { get_star_ts: 1671911371, star_index: 3737338 }, 1126 | }, 1127 | 20: { 1128 | 1: { star_index: 3716639, get_star_ts: 1671874429 }, 1129 | 2: { star_index: 3716767, get_star_ts: 1671874670 }, 1130 | }, 1131 | 25: { 1132 | 2: { get_star_ts: 1671960907, star_index: 3762977 }, 1133 | 1: { star_index: 3762974, get_star_ts: 1671960903 }, 1134 | }, 1135 | 4: { 1136 | 2: { get_star_ts: 1670130820, star_index: 852989 }, 1137 | 1: { star_index: 850372, get_star_ts: 1670130661 }, 1138 | }, 1139 | 11: { 1140 | 1: { get_star_ts: 1670749362, star_index: 2555209 }, 1141 | 2: { star_index: 2564785, get_star_ts: 1670754072 }, 1142 | }, 1143 | 16: { 1144 | 1: { get_star_ts: 1671747481, star_index: 3638420 }, 1145 | 2: { star_index: 3640778, get_star_ts: 1671750740 }, 1146 | }, 1147 | 15: { 1148 | 1: { star_index: 3629567, get_star_ts: 1671735634 }, 1149 | 2: { get_star_ts: 1671736805, star_index: 3630405 }, 1150 | }, 1151 | 12: { 1152 | 2: { star_index: 3503140, get_star_ts: 1671564582 }, 1153 | 1: { star_index: 3502561, get_star_ts: 1671563909 }, 1154 | }, 1155 | 19: { 1156 | 2: { get_star_ts: 1671826631, star_index: 3691380 }, 1157 | 1: { get_star_ts: 1671824815, star_index: 3690133 }, 1158 | }, 1159 | 17: { 1160 | 2: { star_index: 3658231, get_star_ts: 1671781750 }, 1161 | 1: { get_star_ts: 1671777496, star_index: 3654970 }, 1162 | }, 1163 | 14: { 1164 | 2: { get_star_ts: 1671693763, star_index: 3600022 }, 1165 | 1: { get_star_ts: 1671693457, star_index: 3599830 }, 1166 | }, 1167 | 9: { 1168 | 2: { star_index: 2176717, get_star_ts: 1670564336 }, 1169 | 1: { star_index: 2174735, get_star_ts: 1670563962 }, 1170 | }, 1171 | 18: { 1172 | 2: { get_star_ts: 1671791560, star_index: 3665167 }, 1173 | 1: { star_index: 3658598, get_star_ts: 1671782282 }, 1174 | }, 1175 | 10: { 1176 | 1: { get_star_ts: 1670708787, star_index: 2493939 }, 1177 | 2: { get_star_ts: 1670710403, star_index: 2497220 }, 1178 | }, 1179 | 1: { 1180 | 2: { star_index: 1987, get_star_ts: 1669871255 }, 1181 | 1: { star_index: 91, get_star_ts: 1669871152 }, 1182 | }, 1183 | 5: { 1184 | 2: { star_index: 1141095, get_star_ts: 1670217940 }, 1185 | 1: { star_index: 1138973, get_star_ts: 1670217712 }, 1186 | }, 1187 | 2: { 1188 | 2: { get_star_ts: 1669958118, star_index: 271551 }, 1189 | 1: { get_star_ts: 1669957892, star_index: 267788 }, 1190 | }, 1191 | 22: { 1192 | 1: { get_star_ts: 1671899162, star_index: 3731008 }, 1193 | 2: { get_star_ts: 1671907030, star_index: 3735288 }, 1194 | }, 1195 | 7: { 1196 | 2: { star_index: 1740228, get_star_ts: 1670391664 }, 1197 | 1: { star_index: 1738641, get_star_ts: 1670391354 }, 1198 | }, 1199 | 3: { 1200 | 2: { star_index: 587673, get_star_ts: 1670045146 }, 1201 | 1: { star_index: 582914, get_star_ts: 1670044716 }, 1202 | }, 1203 | 24: { 1204 | 1: { star_index: 3745221, get_star_ts: 1671927611 }, 1205 | 2: { star_index: 3745624, get_star_ts: 1671928711 }, 1206 | }, 1207 | 8: { 1208 | 2: { get_star_ts: 1670476994, star_index: 1957590 }, 1209 | 1: { get_star_ts: 1670476167, star_index: 1952605 }, 1210 | }, 1211 | 13: { 1212 | 1: { get_star_ts: 1671632666, star_index: 3558636 }, 1213 | 2: { get_star_ts: 1671633727, star_index: 3559659 }, 1214 | }, 1215 | 21: { 1216 | 2: { get_star_ts: 1671893324, star_index: 3727621 }, 1217 | 1: { get_star_ts: 1671877586, star_index: 3718535 }, 1218 | }, 1219 | 6: { 1220 | 1: { get_star_ts: 1670303039, star_index: 1434675 }, 1221 | 2: { star_index: 1435321, get_star_ts: 1670303067 }, 1222 | }, 1223 | }, 1224 | last_star_ts: 1671960907, 1225 | }, 1226 | 427026: { 1227 | completion_day_level: { 1228 | 4: { 1229 | 2: { star_index: 3692765, get_star_ts: 1671828526 }, 1230 | 1: { get_star_ts: 1671827482, star_index: 3691967 }, 1231 | }, 1232 | 3: { 1233 | 1: { star_index: 3690359, get_star_ts: 1671825150 }, 1234 | 2: { get_star_ts: 1671825963, star_index: 3690949 }, 1235 | }, 1236 | 2: { 1237 | 1: { star_index: 3688320, get_star_ts: 1671822108 }, 1238 | 2: { get_star_ts: 1671822821, star_index: 3688795 }, 1239 | }, 1240 | 1: { 1241 | 2: { get_star_ts: 1671802619, star_index: 3673138 }, 1242 | 1: { get_star_ts: 1671802051, star_index: 3672686 }, 1243 | }, 1244 | 5: { 1245 | 1: { star_index: 3767105, get_star_ts: 1671967425 }, 1246 | 2: { get_star_ts: 1671968476, star_index: 3767730 }, 1247 | }, 1248 | }, 1249 | last_star_ts: 1671968476, 1250 | name: "snap", 1251 | local_score: 179, 1252 | global_score: 0, 1253 | id: 427026, 1254 | stars: 10, 1255 | }, 1256 | 2380830: { 1257 | global_score: 0, 1258 | id: 2380830, 1259 | stars: 7, 1260 | name: "Heeler petunia", 1261 | local_score: 153, 1262 | last_star_ts: 1670439090, 1263 | completion_day_level: { 1264 | 1: { 1265 | 1: { star_index: 455538, get_star_ts: 1669994881 }, 1266 | 2: { star_index: 462606, get_star_ts: 1669996490 }, 1267 | }, 1268 | 2: { 1269 | 2: { star_index: 1282979, get_star_ts: 1670250481 }, 1270 | 1: { get_star_ts: 1670249471, star_index: 1279011 }, 1271 | }, 1272 | 3: { 1273 | 1: { get_star_ts: 1670255543, star_index: 1303681 }, 1274 | 2: { star_index: 1551833, get_star_ts: 1670322229 }, 1275 | }, 1276 | 4: { 1: { get_star_ts: 1670439090, star_index: 1879909 } }, 1277 | }, 1278 | }, 1279 | 2268153: { 1280 | last_star_ts: 1671086373, 1281 | completion_day_level: { 1282 | 13: { 1283 | 1: { get_star_ts: 1671046153, star_index: 3045675 }, 1284 | 2: { get_star_ts: 1671086373, star_index: 3089706 }, 1285 | }, 1286 | 11: { 1287 | 1: { star_index: 2644633, get_star_ts: 1670789068 }, 1288 | 2: { get_star_ts: 1670860547, star_index: 2765064 }, 1289 | }, 1290 | 8: { 1: { star_index: 2100657, get_star_ts: 1670523550 } }, 1291 | 9: { 1292 | 1: { star_index: 2218483, get_star_ts: 1670579200 }, 1293 | 2: { get_star_ts: 1670626865, star_index: 2325177 }, 1294 | }, 1295 | 6: { 1296 | 2: { get_star_ts: 1670309381, star_index: 1487116 }, 1297 | 1: { get_star_ts: 1670309248, star_index: 1486471 }, 1298 | }, 1299 | 2: { 1300 | 2: { star_index: 332424, get_star_ts: 1669968511 }, 1301 | 1: { get_star_ts: 1669968081, star_index: 330180 }, 1302 | }, 1303 | 10: { 1304 | 1: { get_star_ts: 1670664174, star_index: 2392073 }, 1305 | 2: { get_star_ts: 1670666182, star_index: 2397306 }, 1306 | }, 1307 | 1: { 1308 | 2: { star_index: 107435, get_star_ts: 1669898753 }, 1309 | 1: { get_star_ts: 1669897397, star_index: 103082 }, 1310 | }, 1311 | 5: { 1312 | 2: { get_star_ts: 1670246489, star_index: 1267348 }, 1313 | 1: { star_index: 1262880, get_star_ts: 1670245331 }, 1314 | }, 1315 | 4: { 1316 | 1: { get_star_ts: 1670151555, star_index: 948185 }, 1317 | 2: { get_star_ts: 1670151608, star_index: 948433 }, 1318 | }, 1319 | 3: { 1320 | 2: { star_index: 1069348, get_star_ts: 1670183320 }, 1321 | 1: { star_index: 1067316, get_star_ts: 1670182693 }, 1322 | }, 1323 | }, 1324 | global_score: 0, 1325 | id: 2268153, 1326 | stars: 21, 1327 | name: "Swipple nipa", 1328 | local_score: 620, 1329 | }, 1330 | 32698: { 1331 | last_star_ts: 0, 1332 | completion_day_level: {}, 1333 | global_score: 0, 1334 | stars: 0, 1335 | id: 32698, 1336 | local_score: 0, 1337 | name: "Ironwood calves", 1338 | }, 1339 | }, 1340 | event: "2022", 1341 | owner_id: 366773, 1342 | }; 1343 | })((window.aoc = window.aoc || {})); 1344 | --------------------------------------------------------------------------------