├── .gitignore ├── .prettierrc ├── README.md ├── about.html ├── compare.html ├── images ├── android-desktop.png ├── favicon.png ├── icon.png ├── ios-desktop.png └── og.jpg ├── index.html ├── js ├── calculate.js ├── compare.js ├── compare_helper.js ├── single.js └── vir.js ├── manifest.json ├── styles └── style.css ├── sw.js └── virtual-rating-change.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.sublime-project 3 | *.sublime-workspace -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "none", 3 | "singleQuote": true, 4 | "printWidth": 90 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Codeforces Visualizer 2 | 3 | This is code repository for a simple analytics visualization site for [Codeforces online judge](http://codeforces.com/) users using [Codeforces API](https://codeforces.com/apiHelp). The site is currently hosted at [here](https://cfviz.netlify.com/). 4 | 5 | ### Current features 6 | 7 | #### Single User Analytics 8 | * Verdicts chart 9 | * Languages chart 10 | * Tags chart 11 | * Levels chart 12 | * Total tried problems count 13 | * Total solved problems count 14 | * Average and max attempts 15 | * Count of problems solved with one submission 16 | * Max AC for a single problem (It indicates in how many ways someone solved a problem) 17 | * List of unsolved problems 18 | 19 | #### Comparison between two users 20 | * Current, max and min rating 21 | * Number of contests 22 | * Best and worst position in contest 23 | * Max positive and negative rating change 24 | * Compared rating time-line 25 | * Total tried problem count compared 26 | * Total solved problem count compared 27 | * Average and max attempts compared 28 | * Count of problems solved with one submission compared 29 | * Max AC for a single problem compared 30 | * Tags compared 31 | * Levels compared 32 | 33 | 34 | #### Issues 35 | * When somebody searches for a handle that doesn't exists, we get Cross-Origin Request blocked and the status code becomes 0 in jQuery. So we can't determine if the user doesn't really exists or some other network problem occurs. 36 | * Firefox hangs for a while when drawing the tags comparison chart. Probably because it's big. I have plan to divide that chart in two parts. 37 | * When counting number of solved problems, some problems that appear both on div 1 and div 2 get counted twice. -------------------------------------------------------------------------------- /about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Codeforces Visualizer 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 54 | 71 | 72 | 73 | 74 |
75 |
76 |
77 |
78 | Codeforces Visualizer 79 |
80 | 86 |
87 |
88 |
89 | 90 | 96 |
97 |
98 |
99 |
100 |

Thanks!

101 |

Thanks for using. Hope you liked it. Don't forget to share and recommend in that case. 102 | MDL, Google Charts and jQuery was used in this project. Source available here.

103 |

Special thanks to Shakir Ahsan Romeo, Mashpy Says, Ahmed Shamim Hasan Shaon for help, inspiration and contribution. 104 |

105 |

Any opinion, suggetion?

106 |
107 |
108 |
109 |
110 | 121 |
122 |
123 |
124 | 125 | 126 | 127 | 128 | -------------------------------------------------------------------------------- /compare.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Codeforces Visualizer 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 54 | 75 | 76 | 77 | 78 |
79 |
80 |
81 |
82 | Codeforces Visualizer 83 |
84 | 90 |
91 |
92 |
93 | 94 | 100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | 108 | 109 | Couldn't find user. Network problem? 110 |
111 |
112 | 113 | 114 | Couldn't find user. Network problem? 115 |
116 |
117 | 118 |
119 |
120 | 122 | 124 | 126 | 149 | 152 | 154 | 156 | 158 | Average submissions they had to make to solve a single problem 159 | 161 | Maximum submissions they had to make to solve a single problem 162 | 164 | Percentage of problems they've solved with just one submission 165 | 167 | 181 | 183 | 185 | 187 | 201 | 212 |
213 |
214 |
215 | 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /images/android-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjsakib/cfviz/b31e9fb1366873d6d719a0d276debc7d5ec74970/images/android-desktop.png -------------------------------------------------------------------------------- /images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjsakib/cfviz/b31e9fb1366873d6d719a0d276debc7d5ec74970/images/favicon.png -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjsakib/cfviz/b31e9fb1366873d6d719a0d276debc7d5ec74970/images/icon.png -------------------------------------------------------------------------------- /images/ios-desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjsakib/cfviz/b31e9fb1366873d6d719a0d276debc7d5ec74970/images/ios-desktop.png -------------------------------------------------------------------------------- /images/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sjsakib/cfviz/b31e9fb1366873d6d719a0d276debc7d5ec74970/images/og.jpg -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Codeforces Visualizer 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 |
37 |
38 | Codeforces Visualizer 39 |
40 | 46 |
47 |
48 |
49 | 50 | 56 |
57 |
58 |
59 |
60 |
61 |
62 | 67 |
68 |
69 | 71 | 73 | 75 | 77 | Indexes in contests of the problems they have solved. All subindexes like A1, A2 have been merged 78 | 80 | Ratings of the problems they have solved. Problems without ratings are ignored 81 | 122 | 150 | 155 | 165 | 174 |
175 |
176 |
177 | 198 | 215 | 220 | 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /js/calculate.js: -------------------------------------------------------------------------------- 1 | function getEloWinProbability(ra, rb) { 2 | return 1.0 / (1.0 + Math.pow(10.0, (rb - ra) / 400.0)); 3 | } 4 | 5 | function getSeed(contestants, rating) { 6 | if (rating in contestants.memSeed) { 7 | return contestants.memSeed[rating]; 8 | } 9 | var result = 1.0; 10 | for (var i = 0; i < contestants.content.length; i++) { 11 | result += getEloWinProbability(contestants.content[i].rating, rating); 12 | } 13 | contestants.memSeed[rating] = result; 14 | return result; 15 | } 16 | 17 | function getRatingToRank(contestants, realRating, rank) { 18 | var left = 1; 19 | var right = 8000; 20 | while (right - left > 1) { 21 | var mid = parseInt((left + right) / 2); 22 | if (getSeed(contestants, mid) - getEloWinProbability(realRating, mid) < rank) { 23 | right = mid; 24 | } else { 25 | left = mid; 26 | } 27 | } 28 | return left; 29 | } 30 | 31 | function reassignRanks(contestants) { 32 | var first = 0; 33 | var points = contestants.content[0].rank; 34 | for (var i = 1; i < contestants.content.length; i++) { 35 | if (contestants.content[i].rank > points) { 36 | for (var j = first; j < i; j++) { 37 | contestants.content[j].rank = i; 38 | } 39 | first = i; 40 | points = contestants.content[i].rank; 41 | } 42 | } 43 | for (var i = first; i < contestants.content.length; i++) { 44 | contestants.content[i].rank = contestants.content.length; 45 | } 46 | } 47 | 48 | function process(contestants) { 49 | if (contestants.content.length == 0) { 50 | return; 51 | } 52 | reassignRanks(contestants); 53 | for (var i = 0; i < contestants.content.length; i++) { 54 | var contestant = contestants.content[i]; 55 | var rating = contestant.rating; 56 | contestant.seed = getSeed(contestants, rating) - 0.5; 57 | var midRank = Math.sqrt(contestant.rank * contestant.seed); 58 | contestant.needRating = parseInt(getRatingToRank(contestants, rating, midRank)); 59 | contestant.delta = parseInt((contestant.needRating - contestant.rating) / 2); 60 | } 61 | 62 | contestants.content.sort(function (a, b) { 63 | return b.rating - a.rating; 64 | }); 65 | 66 | { 67 | var sum = 0; 68 | for (var i = 0; i < contestants.content.length; i++) { 69 | sum += parseInt(contestants.content[i].delta); 70 | } 71 | var inc = parseInt(-sum / contestants.content.length) - 1; 72 | for (var i = 0; i < contestants.content.length; i++) { 73 | contestants.content[i].delta += inc; 74 | } 75 | console.log(inc); 76 | } 77 | 78 | var sum = 0; 79 | var zeroSumCount = parseInt( 80 | Math.min( 81 | parseInt(4 * Math.round(Math.sqrt(contestants.content.length))), 82 | contestants.content.length 83 | ) 84 | ); 85 | for (var i = 0; i < zeroSumCount; i++) { 86 | sum += contestants.content[i].delta; 87 | } 88 | var inc = parseInt(Math.min(Math.max(parseInt(-sum / zeroSumCount), -10), 0)); 89 | for (var i = 0; i < contestants.content.length; i++) { 90 | contestants.content[i].delta += inc; 91 | } 92 | console.log(inc); 93 | } 94 | 95 | function CalculateRatingChanges(previousRatings, standingsRows, userId) { 96 | var arr = []; 97 | for (var i = 0; i < standingsRows.length; i++) { 98 | var currentContestant = { 99 | party: userId[i], 100 | rank: standingsRows[i], 101 | rating: previousRatings[i], 102 | seed: 0.0, 103 | needRating: 0.0, 104 | delta: 0 105 | }; 106 | arr.push(currentContestant); 107 | } 108 | var memTmp = []; 109 | var contestants = { 110 | content: arr, 111 | memSeed: memTmp 112 | }; 113 | process(contestants); 114 | var result = {}; 115 | for (var i = 0; i < contestants.content.length; i++) { 116 | result[contestants.content[i].party] = contestants.content[i].delta; 117 | } 118 | return contestants.content; 119 | } 120 | -------------------------------------------------------------------------------- /js/compare.js: -------------------------------------------------------------------------------- 1 | var api_url = 'https://codeforces.com/api/'; 2 | var handle1 = ''; 3 | var handle2 = ''; 4 | 5 | var conData1 = {}; // contest data for user 1 6 | var conData2 = {}; // contest data for suer 1 7 | 8 | var subData1 = {}; // submission data for user 1 9 | var subData2 = {}; // submission data for user 2 10 | 11 | var colors = ['#009688', '#3F51B5']; 12 | 13 | var req1, req2, req3, req4; 14 | 15 | google.charts.load('current', { packages: ['corechart'] }); 16 | 17 | $(document).ready(function () { 18 | $('#handleform').submit(function (e) { 19 | e.preventDefault(); 20 | $('#handle1').blur(); 21 | $('#handle2').blur(); 22 | 23 | resetData(); 24 | 25 | handle1 = $('#handle1').val().trim(); 26 | handle2 = $('#handle2').val().trim(); 27 | 28 | if (!handle1) { 29 | err_message('handle2Div', 'Enter a name'); 30 | $('#mainSpinner').removeClass('is-active'); 31 | return; 32 | } 33 | if (!handle2) { 34 | err_message('handle2Div', 'Enter a name'); 35 | $('#mainSpinner').removeClass('is-active'); 36 | return; 37 | } 38 | 39 | //Getting handle1 contest data 40 | req1 = $.get(api_url + 'user.rating', { handle: handle1 }, function (data, status) { 41 | console.log(data); 42 | if (data.result.length > 0) conData1 = getContestStat(data); 43 | else { 44 | err_message('handle1Div', 'No contests'); 45 | conData1 = null; 46 | } 47 | }).fail(function (xhr, status) { 48 | if (status != 'abort') { 49 | err_message('handle1Div', "Couldn't find user"); 50 | $('#mainSpinner').removeClass('is-active'); 51 | } 52 | }); 53 | 54 | //Getting handle2 contest data 55 | req2 = $.get(api_url + 'user.rating', { handle: handle2 }, function (data, status) { 56 | console.log(data); 57 | if (data.result.length > 0) conData2 = getContestStat(data); 58 | else { 59 | err_message('handle2Div', 'No contests'); 60 | conData2 = null; 61 | } 62 | }).fail(function (xhr, status) { 63 | if (status != 'abort') { 64 | err_message('handle2Div', "Couldn't find user"); 65 | $('#mainSpinner').removeClass('is-active'); 66 | } 67 | }); 68 | 69 | $.when(req1, req2).then(function () { 70 | if (typeof google.visualization === 'undefined') { 71 | if (conData1 && conData2) google.charts.setOnLoadCallback(drawConCharts); 72 | } else { 73 | if (conData1 && conData2) drawConCharts(); 74 | } 75 | 76 | // getting handle1 submission data 77 | // firefox doesn't allow more then 3 active connections at a time 78 | // that's why we have to send req3 and req4 when req1 and req2 is done 79 | req3 = $.get(api_url + 'user.status', { handle: handle1 }, function (data, status) { 80 | console.log(data); 81 | if (data.result.length > 0) subData1 = getSubData(data); 82 | else { 83 | err_message('handle1Div', 'No submissions'); 84 | subData1 = null; 85 | } 86 | }); 87 | req4 = $.get(api_url + 'user.status', { handle: handle2 }, function (data, status) { 88 | console.log(data); 89 | if (data.result.length > 0) subData2 = getSubData(data); 90 | else { 91 | err_message('handle2Div', 'No submissions'); 92 | subData2 = null; 93 | } 94 | }); 95 | 96 | $.when(req3, req4).then(function () { 97 | if (typeof google.visualization === 'undefined') { 98 | if (subData1 && subData2) google.charts.setOnLoadCallback(drawSubCharts); 99 | } else { 100 | if (subData1 && subData2) drawSubCharts(); 101 | } 102 | $('.share-div').removeClass('hidden'); 103 | $('#mainSpinner').removeClass('is-active'); 104 | $('.sharethis').removeClass('hidden'); 105 | }); 106 | }); 107 | }); 108 | 109 | handle1 = getParameterByName('handle1'); 110 | handle2 = getParameterByName('handle2'); 111 | if (handle1 !== null && handle2 !== null) { 112 | $('#handle1').val(handle1); 113 | $('#handle2').val(handle2); 114 | $('#handleform').submit(); 115 | } 116 | $('#handleDiv').removeClass('hidden'); 117 | }); 118 | 119 | // draw contest related charts, those can be done when req1 and req2 is complete 120 | function drawConCharts() { 121 | //Rating 122 | var rating = new google.visualization.arrayToDataTable([ 123 | ['Handle', handle1, handle2], 124 | ['Current Rating', conData1.rating, conData2.rating], 125 | ['Max Rating', conData1.maxRating, conData2.maxRating], 126 | ['Min Rating', conData1.minRating, conData2.minRating] 127 | ]); 128 | var ratingOptions = $.extend({}, commonOptions, { 129 | legend: legend, 130 | colors: colors, 131 | vAxis: { 132 | minValue: 0 133 | } 134 | }); 135 | var ratingChart = new google.visualization.ColumnChart( 136 | document.getElementById('ratings') 137 | ); 138 | $('#ratings').removeClass('hidden'); 139 | ratingChart.draw(rating, ratingOptions); 140 | 141 | // Contests Count 142 | plotTwo('contestsCount', conData1.tot, conData2.tot, 'Contests'); 143 | 144 | // Max up and downs 145 | var upDowns = new google.visualization.arrayToDataTable([ 146 | ['Handle', handle1, handle2], 147 | ['Max Up', conData1.maxUp, conData2.maxUp], 148 | ['Max Down', conData1.maxDown, conData2.maxDown] 149 | ]); 150 | var upDownsOptions = $.extend({}, commonOptions, { 151 | legend: legend, 152 | colors: colors 153 | }); 154 | var upDownsChart = new google.visualization.ColumnChart( 155 | document.getElementById('upDowns') 156 | ); 157 | $('#upDowns').removeClass('hidden'); 158 | upDownsChart.draw(upDowns, upDownsOptions); 159 | 160 | //Worst Best 161 | $('#bestWorst').removeClass('hidden'); 162 | $('#user1').html(handle1); 163 | $('#user2').html(handle2); 164 | $('#user1Best').html(conData1.best); 165 | $('#user2Best').html(conData2.best); 166 | $('#user1Worst').html(conData1.worst); 167 | $('#user2Worst').html(conData2.worst); 168 | 169 | // Rating Timeline 170 | var timeline = new google.visualization.DataTable(); 171 | timeline.addColumn('date', 'Date'); 172 | timeline.addColumn('number', handle1); 173 | timeline.addColumn('number', handle2); 174 | 175 | timeline.addRows(alignTimeline(conData1.timeline, conData2.timeline)); 176 | 177 | $('#timelineCon').removeClass('hidden'); 178 | var timelineOptions = $.extend({}, commonOptions, scrollableOptions, { 179 | title: 'Timeline', 180 | legend: legend, 181 | width: Math.max(timeline.getNumberOfRows() * 7, $('#timelineCon').width()), 182 | height: 400, 183 | hAxis: { 184 | format: 'MMM yyyy' 185 | }, 186 | vAxis: { 187 | viewWindowMode: 'pretty' 188 | }, 189 | colors: colors, 190 | curveType: 'function' 191 | }); 192 | var timelineChart = new google.visualization.LineChart( 193 | document.getElementById('timeline') 194 | ); 195 | timelineChart.draw(timeline, timelineOptions); 196 | 197 | // Common Contests 198 | $('#commonContestsCon').removeClass('hidden'); 199 | $('#user1Con').html(handle1); 200 | $('#user2Con').html(handle2); 201 | var con_url = 'https://codeforces.com/contest/'; 202 | var commonContests = getCommonContests(conData1.all, conData2.all); 203 | commonContests.sort(function (a, b) { 204 | return a.contestId - b.contestId; 205 | }); 206 | commonContests.forEach(function (con) { 207 | var handle1El = '' + con.handle1 + ''; 208 | var handle2El = '' + con.handle2 + ''; 209 | var dis = con.handle2 - con.handle1; 210 | dis = 211 | dis > 0 212 | ? '' + Math.abs(dis) + '' 213 | : '' + Math.abs(dis) + ''; 214 | $('#commonContestList').append( 215 | '' + 219 | con.contestName + 220 | '' + 221 | handle1El + 222 | handle2El + 223 | dis + 224 | '' 225 | ); 226 | }); 227 | if (commonContests.length === 0) { 228 | $('#commonContestList').append('No common contests'); 229 | } 230 | } 231 | 232 | // draw the charts that need all the submission data of two users 233 | function drawSubCharts() { 234 | // Tried and solved 235 | var solvedTried = new google.visualization.arrayToDataTable([ 236 | ['Handle', handle1, handle2], 237 | ['Tried', subData1.tried, subData2.tried], 238 | ['Solved', subData1.solved, subData2.solved] 239 | ]); 240 | var solvedTriedOptions = $.extend({}, commonOptions, { 241 | legend: legend, 242 | colors: colors, 243 | vAxis: { 244 | minValue: 0 245 | } 246 | }); 247 | var solvedTriedChart = new google.visualization.ColumnChart( 248 | document.getElementById('solvedTried') 249 | ); 250 | $('#solvedTried').removeClass('hidden'); 251 | solvedTriedChart.draw(solvedTried, solvedTriedOptions); 252 | 253 | plotTwo('unsolved', subData1.unsolved, subData2.unsolved, 'Unsolved'); 254 | plotTwo('averageSub', subData1.averageSub, subData2.averageSub, 'Average Submission'); 255 | plotTwo('maxSub', subData1.maxSub, subData2.maxSub, 'Max submission'); 256 | plotTwo('maxAc', subData1.maxAc, subData2.maxAc, 'Max AC'); 257 | plotTwo( 258 | 'oneSub', 259 | subData1.solved ? (subData1.solvedWithOneSub / subData1.solved) * 100 : 0, 260 | subData2.solved ? (subData2.solvedWithOneSub / subData2.solved) * 100 : 0, 261 | 'Solved with one submission (%)' 262 | ); 263 | 264 | // Common Solved 265 | $('#commonSolvedTable').removeClass('hidden'); 266 | var commonSolved = $(subData1.problems).filter(subData2.problems).length; 267 | $('#commonSolved').html(commonSolved); 268 | 269 | // levels 270 | $('#levels').removeClass('hidden'); 271 | var levels = new google.visualization.DataTable(); 272 | levels.addColumn('string', 'Index'); 273 | levels.addColumn('number', handle1); 274 | levels.addColumn('number', handle2); 275 | levels.addRows(alignLevels(subData1.levels, subData2.levels)); 276 | var levelsView = new google.visualization.DataView(levels); 277 | levelsView.setColumns([ 278 | 0, 279 | 1, 280 | { 281 | calc: 'stringify', 282 | sourceColumn: 1, 283 | type: 'string', 284 | role: 'annotation' 285 | }, 286 | 2, 287 | { 288 | calc: 'stringify', 289 | sourceColumn: 2, 290 | type: 'string', 291 | role: 'annotation' 292 | } 293 | ]); 294 | 295 | var levelsOptions = $.extend({}, scrollableOptions, commonOptions, { 296 | width: Math.max($('#levels').width(), levels.getNumberOfRows() * 65), 297 | height: 400, 298 | title: 'Levels', 299 | legend: legend, 300 | colors: colors, 301 | bar: { groupWidth: '65%' }, 302 | annotations: annotation 303 | }); 304 | var levelsChart = new google.visualization.ColumnChart( 305 | document.getElementById('levels') 306 | ); 307 | levelsChart.draw(levelsView, levelsOptions); 308 | 309 | /* Problem Ratings */ 310 | $('#pRatings').removeClass('hidden'); 311 | var pRatings = new google.visualization.DataTable(); 312 | pRatings.addColumn('string', 'Rating'); 313 | pRatings.addColumn('number', handle1); 314 | pRatings.addColumn('number', handle2); 315 | pRatings.addRows(alignPRatings(subData1.pRatings, subData2.pRatings)); 316 | var pRatingsView = new google.visualization.DataView(pRatings); 317 | pRatingsView.setColumns([ 318 | 0, 319 | 1, 320 | { 321 | calc: 'stringify', 322 | sourceColumn: 1, 323 | type: 'string', 324 | role: 'annotation' 325 | }, 326 | 2, 327 | { 328 | calc: 'stringify', 329 | sourceColumn: 2, 330 | type: 'string', 331 | role: 'annotation' 332 | } 333 | ]); 334 | 335 | var pRatingsOptions = $.extend({}, scrollableOptions, commonOptions, { 336 | width: Math.max($('#pRatings').width(), pRatings.getNumberOfRows() * 65), 337 | height: 400, 338 | title: 'Problem Ratings', 339 | legend: legend, 340 | colors: colors, 341 | bar: { groupWidth: '65%' }, 342 | annotations: annotation 343 | }); 344 | var pRatingsChart = new google.visualization.ColumnChart( 345 | document.getElementById('pRatings') 346 | ); 347 | pRatingsChart.draw(pRatingsView, pRatingsOptions); 348 | 349 | //Tags chart 350 | $('#tags').removeClass('hidden'); 351 | var tags = new google.visualization.DataTable(); 352 | tags.addColumn('string', 'Index'); 353 | tags.addColumn('number', handle1); 354 | tags.addColumn('number', handle2); 355 | tags.addRows(alignTags(subData1.tags, subData2.tags)); 356 | var tagsView = new google.visualization.DataView(tags); 357 | tagsView.setColumns([ 358 | 0, 359 | 1, 360 | { 361 | calc: 'stringify', 362 | sourceColumn: 1, 363 | type: 'string', 364 | role: 'annotation' 365 | }, 366 | 2, 367 | { 368 | calc: 'stringify', 369 | sourceColumn: 2, 370 | type: 'string', 371 | role: 'annotation' 372 | } 373 | ]); 374 | var tagsOptions = $.extend({}, scrollableOptions, commonOptions, { 375 | width: Math.max($('#tags').width(), tags.getNumberOfRows() * 75), 376 | height: 400, 377 | title: 'Tags', 378 | legend: legend, 379 | colors: colors, 380 | bar: { groupWidth: '60%' }, 381 | annotations: annotation, 382 | chartArea: { top: 100, bottom: 120, left: 100, right: 75 } 383 | }); 384 | var tagsChart = new google.visualization.ColumnChart(document.getElementById('tags')); 385 | tagsChart.draw(tagsView, tagsOptions); 386 | } 387 | 388 | // when we need to compare two numbers, we can use this function 389 | // it takes the two numbers, a title and a div. then draws a column chart in that div comparing the numbers 390 | function plotTwo(div, n1, n2, title) { 391 | if (!(n1 || n2)) return; 392 | var table = new google.visualization.arrayToDataTable([ 393 | ['Handle', title, { role: 'style' }], 394 | [handle1, n1, colors[0]], 395 | [handle2, n2, colors[1]] 396 | ]); 397 | var options = $.extend({}, commonOptions, { 398 | title: title, 399 | vAxis: { 400 | minValue: 0 401 | }, 402 | legend: 'none' 403 | }); 404 | var chart = new google.visualization.ColumnChart(document.getElementById(div)); 405 | $('#' + div).removeClass('hidden'); 406 | chart.draw(table, options); 407 | } 408 | 409 | function resetData() { 410 | $('#mainSpinner').addClass('is-active'); 411 | $('.to-clear').empty(); 412 | $('.to-hide').addClass('hidden'); 413 | 414 | if (req1) req1.abort(); 415 | if (req2) req2.abort(); 416 | if (req3) req3.abort(); 417 | if (req4) req4.abort(); 418 | } 419 | 420 | function get_url(p) { 421 | var con = p.split('-')[0]; 422 | var index = p.split('-')[1]; 423 | 424 | var url = ''; 425 | if (con.length < 4) url = 'https://codeforces.com/contest/' + con + '/problem/' + index; 426 | else url = 'https://codeforces.com/problemset/gymProblem/' + con + '/' + index; 427 | 428 | return url; 429 | } 430 | 431 | //Copied from stackoverflow :D 432 | function getParameterByName(name, url) { 433 | if (!url) { 434 | url = window.location.href; 435 | } 436 | name = name.replace(/[\[\]]/g, '\\$&'); 437 | var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), 438 | results = regex.exec(url); 439 | if (!results) return null; 440 | if (!results[2]) return ''; 441 | return decodeURIComponent(results[2].replace(/\+/g, ' ')); 442 | } 443 | 444 | function fbShareResult() { 445 | var url; 446 | if (handle1 && handle2) 447 | url = window.location.href + '?handle1=' + handle1 + '&handle2=' + handle2; 448 | else url = window.location.href; 449 | window.open( 450 | 'https://www.facebook.com/sharer/sharer.php?u=' + escape(url), 451 | '', 452 | 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=300,width=600' 453 | ); 454 | } 455 | -------------------------------------------------------------------------------- /js/compare_helper.js: -------------------------------------------------------------------------------- 1 | var MAX_TIME_DIFF = 7200; // max time between contests 2 | 3 | //common Options for charts 4 | var legend = { 5 | position: 'top', 6 | alignment: 'end' 7 | }; 8 | 9 | var commonOptions = { 10 | height: 300, 11 | titleTextStyle: { 12 | fontSize: 18, 13 | color: '#393939', 14 | bold: false 15 | }, 16 | fontName: 'Roboto', 17 | bar: { groupWidth: '30%' }, 18 | legend: { 19 | position: 'top', 20 | alignment: 'end' 21 | }, 22 | animation: { 23 | easing: 'in', 24 | startup: true 25 | }, 26 | tooltip: { 27 | textStyle: { fontSize: 14 } 28 | } 29 | }; 30 | 31 | var scrollableOptions = { 32 | chartArea: { top: 100, bottom: 80, left: 100, right: 75 }, 33 | vAxis: { 34 | textStyle: { fontSize: 14 } 35 | }, 36 | hAxis: { 37 | textStyle: { fontSize: 14 } 38 | } 39 | }; 40 | 41 | var annotation = { 42 | alwaysOutside: true, 43 | textStyle: { 44 | fontSize: 10 45 | } 46 | }; 47 | 48 | // helper functions, partially copied from single.js 49 | 50 | function getSubData(data) { 51 | var ret = {}; // the object to return 52 | ret.levels = {}; 53 | ret.pRatings = {}; 54 | ret.tags = {}; 55 | var problems = {}; 56 | 57 | // parsing all the submissions and saving useful data 58 | for (var i = data.result.length - 1; i >= 0; i--) { 59 | var sub = data.result[i]; 60 | var problemId = sub.problem.contestId + '-' + sub.problem.index; 61 | if (problems[problemId] === undefined) { 62 | problems[problemId] = { 63 | subs: 1, 64 | solved: 0 65 | }; 66 | } else { 67 | if (problems[problemId].solved === 0) problems[problemId].subs++; 68 | } 69 | 70 | if (sub.verdict == 'OK') { 71 | problems[problemId].solved++; 72 | } 73 | 74 | if (problems[problemId].solved === 1 && sub.verdict == 'OK') { 75 | sub.problem.tags.forEach(function (t) { 76 | if (ret.tags[t] === undefined) ret.tags[t] = 1; 77 | else ret.tags[t]++; 78 | }); 79 | 80 | if (ret.levels[sub.problem.index[0]] === undefined) 81 | ret.levels[sub.problem.index[0]] = 1; 82 | else ret.levels[sub.problem.index[0]]++; 83 | 84 | if (sub.problem.rating) { 85 | ret.pRatings[sub.problem.rating] = ret.pRatings[sub.problem.rating] + 1 || 1; 86 | } 87 | } 88 | } 89 | ret.totalSub = data.result.length; 90 | ret.tried = 0; 91 | ret.solved = 0; 92 | ret.maxSub = 0; 93 | ret.maxAc = 0; 94 | ret.unsolved = 0; 95 | ret.solvedWithOneSub = 0; 96 | for (var p in problems) { 97 | ret.tried++; 98 | if (problems[p].solved > 0) ret.solved++; 99 | if (problems[p].solved === 0) ret.unsolved++; 100 | 101 | ret.maxSub = Math.max(ret.maxSub, problems[p].subs); 102 | ret.maxAc = Math.max(ret.maxAc, problems[p].solved); 103 | 104 | if (problems[p].solved == problems[p].subs) ret.solvedWithOneSub++; 105 | } 106 | ret.averageSub = ret.totalSub / ret.solved; 107 | ret.problems = Object.keys(problems); 108 | 109 | return ret; 110 | } 111 | 112 | // align levels of solved problems for two users 113 | // if one user have solved no problems of a level and other user have, 114 | // we need to put 0 for the first user and the level 115 | function alignLevels(lev1, lev2) { 116 | var ret = []; 117 | for (var l in lev1) { 118 | if (lev2[l] === undefined) ret.push([l, lev1[l], 0]); 119 | else { 120 | ret.push([l, lev1[l], lev2[l]]); 121 | delete lev2[l]; 122 | } 123 | } 124 | for (l in lev2) { 125 | ret.push([l, 0, lev2[l]]); 126 | } 127 | ret.sort(function (a, b) { 128 | if (a[0] < b[0]) return -1; 129 | return 1; 130 | }); 131 | return ret; 132 | } 133 | 134 | function alignPRatings(lev1, lev2) { 135 | var ret = []; 136 | for (var l in lev1) { 137 | if (lev2[l] === undefined) ret.push([l, lev1[l], 0]); 138 | else { 139 | ret.push([l, lev1[l], lev2[l]]); 140 | delete lev2[l]; 141 | } 142 | } 143 | for (l in lev2) { 144 | ret.push([l, 0, lev2[l]]); 145 | } 146 | ret.sort(function (a, b) { 147 | if (parseInt(a[0]) < parseInt(b[0])) return -1; 148 | return 1; 149 | }); 150 | return ret; 151 | } 152 | 153 | // aligns tags 154 | function alignTags(tags1, tags2) { 155 | var ret = []; 156 | for (var t in tags1) { 157 | if (tags2[t] === undefined) ret.push([t, tags1[t], 0]); 158 | else { 159 | ret.push([t, tags1[t], tags2[t]]); 160 | delete tags2[t]; 161 | } 162 | } 163 | for (t in tags2) { 164 | ret.push([t, 0, tags2[t]]); 165 | } 166 | ret.sort(function (a, b) { 167 | if (a[1] + a[2] < b[1] + b[2]) return 1; 168 | return -1; 169 | }); 170 | return ret; 171 | } 172 | 173 | // returns common contests of two users 174 | function getCommonContests(lst1, lst2) { 175 | var ret = []; 176 | for (var con in lst1) { 177 | if (lst2[con] !== undefined) { 178 | ret.push({ 179 | contestId: con, 180 | // there might be
tag in problem names, we need re replace them 181 | contestName: lst1[con][0].replace(new RegExp('
', 'g'), ' - '), 182 | handle1: lst1[con][1], 183 | handle2: lst2[con][1] 184 | }); 185 | } 186 | } 187 | return ret; 188 | } 189 | 190 | // parse all the contests and save useful data 191 | function getContestStat(data) { 192 | var ret = {}; 193 | ret.best = 1e10; 194 | ret.worst = -1e10; 195 | ret.maxUp = 0; 196 | ret.maxDown = 0; 197 | ret.bestCon = ''; 198 | ret.worstCon = ''; 199 | ret.maxUpCon = ''; 200 | ret.maxDownCon = ''; 201 | ret.maxRating = 0; 202 | ret.minRating = 1e10; 203 | ret.rating = 0; 204 | ret.tot = data.result.length; 205 | ret.timeline = []; 206 | ret.all = {}; 207 | 208 | for (var i = 0; i < data.result.length; i++) { 209 | var con = data.result[i]; 210 | ret.all[con.contestId] = [con.contestName, con.rank]; 211 | if (con.rank < ret.best) { 212 | ret.best = con.rank; 213 | ret.bestCon = con.contestId; 214 | } 215 | if (con.rank > ret.worst) { 216 | ret.worst = con.rank; 217 | ret.worstCon = con.contestId; 218 | } 219 | var ch = con.newRating - con.oldRating; 220 | if (ch > ret.maxUp) { 221 | ret.maxUp = ch; 222 | ret.maxUpCon = con.contestId; 223 | } 224 | if (ch < ret.maxDown) { 225 | ret.maxDown = ch; 226 | ret.maxDownCon = con.contestId; 227 | } 228 | 229 | ret.maxRating = Math.max(ret.maxRating, con.newRating); 230 | ret.minRating = Math.min(ret.minRating, con.newRating); 231 | 232 | if (i == data.result.length - 1) ret.rating = con.newRating; 233 | 234 | ret.timeline.push([con.ratingUpdateTimeSeconds, con.newRating]); 235 | } 236 | 237 | return ret; 238 | } 239 | 240 | // align timeline, 241 | // one user might have done a contest and other might haven't 242 | // we need to add a point for the one who hasn't, what his rating was in that time 243 | function alignTimeline(r1, r2) { 244 | ret = []; 245 | var i = 0; 246 | var j = 0; 247 | while (i <= r1.length || j <= r2.length) { 248 | if (compDate(r1[i][0], r2[j][0]) === 0) { 249 | ret.push([new Date(r1[i][0] * 1000), r1[i][1], r2[j][1]]); 250 | i++; 251 | j++; 252 | } else if (compDate(r1[i][0], r2[j][0]) < 0) { 253 | if (j === 0) ret.push([new Date(r1[i][0] * 1000), r1[i][1], null]); 254 | else ret.push([new Date(r1[i][0] * 1000), r1[i][1], r2[j - 1][1]]); 255 | i++; 256 | } else { 257 | if (i === 0) ret.push([new Date(r2[j][0] * 1000), null, r2[j][1]]); 258 | else ret.push([new Date(r2[j][0] * 1000), r1[i - 1][1], r2[j][1]]); 259 | j++; 260 | } 261 | 262 | if (i == r1.length) { 263 | while (j < r2.length) { 264 | ret.push([new Date(r2[j][0] * 1000), r1[i - 1][1], r2[j][1]]); 265 | j++; 266 | } 267 | break; 268 | } 269 | if (j == r2.length) { 270 | while (i < r1.length) { 271 | ret.push([new Date(r1[i][0] * 1000), r1[i][1], r2[j - 1][1]]); 272 | i++; 273 | } 274 | break; 275 | } 276 | } 277 | return ret; 278 | } 279 | 280 | function compDate(d1, d2) { 281 | if (Math.abs(d1 - d2) < MAX_TIME_DIFF) { 282 | return 0; 283 | } 284 | return d1 - d2; 285 | } 286 | 287 | function err_message(div, msg) { 288 | $('#' + div + 'Err').html(msg); 289 | $('#' + div).addClass('is-invalid'); 290 | } 291 | -------------------------------------------------------------------------------- /js/single.js: -------------------------------------------------------------------------------- 1 | var api_url = 'https://codeforces.com/api/'; 2 | var handle = ''; 3 | 4 | var verdicts = {}; 5 | var langs = {}; 6 | var tags = {}; 7 | var levels = {}; 8 | var ratings = {}; 9 | var problems = {}; 10 | var totalSub = 0; 11 | var heatmap = {}; 12 | var heatmapData = {}; 13 | var years = 0; 14 | 15 | var req1, req2; 16 | 17 | var titleTextStyle = { 18 | fontSize: 18, 19 | color: '#393939', 20 | bold: false 21 | }; 22 | 23 | google.charts.load('current', { packages: ['corechart', 'calendar'] }); 24 | 25 | $(document).ready(function () { 26 | // When the handle form is submitted, this function is called... 27 | $('#handleform').submit(function (e) { 28 | e.preventDefault(); 29 | $('#handle').blur(); 30 | resetData(); // When a new submission is made, clear all the previous data and graphs 31 | 32 | handle = $('#handle').val().trim(); 33 | 34 | if (!handle) { 35 | err_message('handleDiv', 'Enter a name'); 36 | $('#mainSpinner').removeClass('is-active'); 37 | return; // No handle is provided, we can't do anything. 38 | } 39 | 40 | // getting all the submissions of a user 41 | req1 = $.get(api_url + 'user.status', { handle: handle }, function (data, status) { 42 | 43 | $('.sharethis').removeClass('hidden'); 44 | 45 | if (data.result.length < 1) { 46 | err_message('handleDiv', 'No submissions'); 47 | return; 48 | } 49 | 50 | // parsing all the submission and saving useful data. Don't remember why from the back 51 | for (var i = data.result.length - 1; i >= 0; i--) { 52 | var sub = data.result[i]; 53 | 54 | // creating unique key for problem {contestID + problem name + problem rating} 55 | var rating; 56 | if (sub.problem.rating === undefined) { 57 | rating = 0; 58 | } else { 59 | rating = sub.problem.rating; 60 | } 61 | 62 | var problemId = sub.problem.contestId + '-' + sub.problem.name + '-' + rating; 63 | 64 | // previous id for removing duplicates 65 | var problemIdprev = 66 | sub.problem.contestId - 1 + '-' + sub.problem.name + '-' + rating; 67 | 68 | // next id for removing duplicates 69 | var problemIdnext = 70 | sub.problem.contestId + 1 + '-' + sub.problem.name + '-' + rating; 71 | 72 | // checking if problem previously visited 73 | if (problems[problemIdprev] !== undefined) { 74 | if (problems[problemIdprev].solved === 0) { 75 | problems[problemIdprev].attempts++; 76 | } 77 | problemId = problemIdprev; 78 | } else if (problems[problemIdnext] !== undefined) { 79 | if (problems[problemIdnext].solved === 0) { 80 | problems[problemIdnext].attempts++; 81 | } 82 | problemId = problemIdnext; 83 | } else if (problems[problemId] !== undefined) { 84 | if (problems[problemId].solved === 0) { 85 | problems[problemId].attempts++; 86 | } 87 | } else { 88 | problems[problemId] = { 89 | problemlink: sub.contestId + '-' + sub.problem.index, // link of problem 90 | attempts: 1, 91 | solved: 0 // We also want to save how many submission got AC, a better name would have been number_of_ac 92 | }; 93 | } 94 | 95 | if (sub.verdict == 'OK') { 96 | problems[problemId].solved++; 97 | } 98 | 99 | // modifying level, rating, and tag counter on first AC. 100 | if (problems[problemId].solved === 1 && sub.verdict == 'OK') { 101 | sub.problem.tags.forEach(function (t) { 102 | if (tags[t] === undefined) tags[t] = 1; 103 | else tags[t]++; 104 | }); 105 | 106 | if (levels[sub.problem.index[0]] === undefined) 107 | levels[sub.problem.index[0]] = 1; 108 | else levels[sub.problem.index[0]]++; 109 | 110 | if (sub.problem.rating) { 111 | if (ratings[sub.problem.rating] === undefined) { 112 | ratings[sub.problem.rating] = 1; 113 | } else { 114 | ratings[sub.problem.rating]++; 115 | } 116 | } 117 | } 118 | 119 | // changing counter of verdict submission 120 | if (verdicts[sub.verdict] === undefined) verdicts[sub.verdict] = 1; 121 | else verdicts[sub.verdict]++; 122 | 123 | // changing counter of launguage submission 124 | if (langs[sub.programmingLanguage] === undefined) 125 | langs[sub.programmingLanguage] = 1; 126 | else langs[sub.programmingLanguage]++; 127 | 128 | //updating the heatmap 129 | var date = new Date(sub.creationTimeSeconds * 1000); // submission date 130 | date.setHours(0, 0, 0, 0); 131 | if (heatmap[date.valueOf()] === undefined) heatmap[date.valueOf()] = 1; 132 | else heatmap[date.valueOf()]++; 133 | totalSub = data.result.length; 134 | 135 | // how many years are there between first and last submission 136 | years = 137 | new Date(data.result[0].creationTimeSeconds * 1000).getYear() - 138 | new Date( 139 | data.result[data.result.length - 1].creationTimeSeconds * 1000 140 | ).getYear(); 141 | years = Math.abs(years) + 1; 142 | } 143 | 144 | // finally draw the charts if google charts is already loaded, 145 | // if not set load callback to draw the charts 146 | if (typeof google.visualization === 'undefined') { 147 | google.charts.setOnLoadCallback(drawCharts); 148 | } else { 149 | drawCharts(); 150 | } 151 | }) 152 | .fail(function (xhr, status) { 153 | //console.log(xhr.status); 154 | if (status != 'abort') err_message('handleDiv', "Couldn't find user"); 155 | }) 156 | .always(function () { 157 | $('#mainSpinner').removeClass('is-active'); 158 | $('.share-div').removeClass('hidden'); 159 | }); 160 | 161 | // With this request we get all the rating changes of the user 162 | req2 = $.get(api_url + 'user.rating', { handle: handle }, function (data, status) { 163 | 164 | if (data.result.length < 1) { 165 | err_message('handleDiv', 'No contests'); 166 | return; 167 | } 168 | var best = 1e10; 169 | var worst = -1e10; 170 | var maxUp = 0; 171 | var maxDown = 0; 172 | var bestCon = ''; 173 | var worstCon = ''; 174 | var maxUpCon = ''; 175 | var maxDownCon = ''; 176 | var tot = data.result.length; 177 | 178 | data.result.forEach(function (con) { 179 | // con is a contest 180 | if (con.rank < best) { 181 | best = con.rank; 182 | bestCon = con.contestId; 183 | } 184 | if (con.rank > worst) { 185 | worst = con.rank; 186 | worstCon = con.contestId; 187 | } 188 | var ch = con.newRating - con.oldRating; 189 | if (ch > maxUp) { 190 | maxUp = ch; 191 | maxUpCon = con.contestId; 192 | } 193 | if (ch < maxDown) { 194 | maxDown = ch; 195 | maxDownCon = con.contestId; 196 | } 197 | }); 198 | 199 | // Showing the rating change data in proper places 200 | var con_url = 'https://codeforces.com/contest/'; 201 | $('#contests').removeClass('hidden'); 202 | $('.handle-text').html(handle); 203 | $('#contestCount').html(tot); 204 | $('#best').html( 205 | best + 206 | ' (' + 210 | bestCon + 211 | ') ' 212 | ); 213 | $('#worst').html( 214 | worst + 215 | ' (' + 219 | worstCon + 220 | ') ' 221 | ); 222 | $('#maxUp').html( 223 | maxUp + 224 | ' (' + 228 | maxUpCon + 229 | ') ' 230 | ); 231 | $('#maxDown').html( 232 | maxDown 233 | ? maxDown + 234 | ' (' + 238 | maxDownCon + 239 | ') ' 240 | : '---' 241 | ); 242 | }); 243 | }); 244 | 245 | // If there is a handle parameter in the url, we'll put it in the form 246 | // and automatically submit it to trigger the submit function, useful for sharing results 247 | handle = getParameterByName('handle'); 248 | if (handle !== null) { 249 | $('#handle').val(handle); 250 | $('#handleform').submit(); 251 | } 252 | $('#handleDiv').removeClass('hidden'); 253 | 254 | // this is to update the heatmap when the form is submitted, contributed 255 | $('#heatmapCon input').keypress(function (e) { 256 | var value = $(this).val(); 257 | //Enter pressed 258 | if (e.which == 13 && value >= 0 && value <= 999) { 259 | var heatmapOptions = { 260 | height: years * 140 + 30, 261 | width: Math.max($('#heatmapCon').width(), 900), 262 | fontName: 'Roboto', 263 | titleTextStyle: titleTextStyle, 264 | colorAxis: { 265 | minValue: 0, 266 | maxValue: value, 267 | colors: ['#ffffff', '#0027ff', '#00127d'] 268 | }, 269 | calendar: { 270 | cellSize: 15 271 | } 272 | }; 273 | heatmap.draw(heatmapData, heatmapOptions); 274 | } 275 | }); 276 | }); 277 | 278 | function drawCharts() { 279 | //Plotting the verdicts chart 280 | $('#verdicts').removeClass('hidden'); 281 | var verTable = [['Verdict', 'Count']]; 282 | var verSliceColors = []; 283 | // beautiful names for the verdicts + colors 284 | for (var ver in verdicts) { 285 | if (ver == 'OK') { 286 | verTable.push(['AC', verdicts[ver]]); 287 | verSliceColors.push({ color: '#4CAF50' }); 288 | } else if (ver == 'WRONG_ANSWER') { 289 | verTable.push(['WA', verdicts[ver]]); 290 | verSliceColors.push({ color: '#f44336' }); 291 | } else if (ver == 'TIME_LIMIT_EXCEEDED') { 292 | verTable.push(['TLE', verdicts[ver]]); 293 | verSliceColors.push({ color: '#2196F3' }); 294 | } else if (ver == 'MEMORY_LIMIT_EXCEEDED') { 295 | verTable.push(['MLE', verdicts[ver]]); 296 | verSliceColors.push({ color: '#673AB7' }); 297 | } else if (ver == 'RUNTIME_ERROR') { 298 | verTable.push(['RTE', verdicts[ver]]); 299 | verSliceColors.push({ color: '#FF5722' }); 300 | } else if (ver == 'COMPILATION_ERROR') { 301 | verTable.push(['CPE', verdicts[ver]]); 302 | verSliceColors.push({ color: '#607D8B' }); 303 | } else if (ver == 'SKIPPED') { 304 | verTable.push(['SKIPPED', verdicts[ver]]); 305 | verSliceColors.push({ color: '#EEEEEE' }); 306 | } else if (ver == 'CLALLENGED') { 307 | verTable.push(['CLALLENGED', verdicts[ver]]); 308 | verSliceColors.push({ color: '#E91E63' }); 309 | } else { 310 | verTable.push([ver, verdicts[ver]]); 311 | verSliceColors.push({}); 312 | } 313 | } 314 | verdicts = new google.visualization.arrayToDataTable(verTable); 315 | var verOptions = { 316 | height: $('#verdicts').width(), 317 | title: 'Verdicts of ' + handle, 318 | legend: 'none', 319 | pieSliceText: 'label', 320 | slices: verSliceColors, 321 | fontName: 'Roboto', 322 | titleTextStyle: titleTextStyle, 323 | is3D: true 324 | }; 325 | var verChart = new google.visualization.PieChart(document.getElementById('verdicts')); 326 | verChart.draw(verdicts, verOptions); 327 | 328 | //Plotting the languages chart 329 | var colors = [ 330 | '#f44336', 331 | '#E91E63', 332 | '#9C27B0', 333 | '#673AB7', 334 | '#2196F3', 335 | '#009688', 336 | '#8BC34A', 337 | '#CDDC39', 338 | '#FFC107', 339 | '#FF9800', 340 | '#FF5722', 341 | '#795548', 342 | '#607D8B', 343 | '#E65100', 344 | '#827717', 345 | '#004D40', 346 | '#1A237E', 347 | '#6200EA', 348 | '#3F51B5', 349 | '#F50057', 350 | '#304FFE', 351 | '#b71c1c' 352 | ]; 353 | 354 | $('#langs').removeClass('hidden'); 355 | var langTable = [['Language', 'Count']]; 356 | for (var lang in langs) { 357 | langTable.push([lang, langs[lang]]); 358 | } 359 | langs = new google.visualization.arrayToDataTable(langTable); 360 | var langOptions = { 361 | height: $('#langs').width(), 362 | title: 'Languages of ' + handle, 363 | legend: 'none', 364 | pieSliceText: 'label', 365 | fontName: 'Roboto', 366 | titleTextStyle: titleTextStyle, 367 | is3D: true, 368 | colors: colors.slice(0, Math.min(colors.length, langs.getNumberOfRows())) 369 | }; 370 | var langChart = new google.visualization.PieChart(document.getElementById('langs')); 371 | langChart.draw(langs, langOptions); 372 | 373 | //the tags chart 374 | $('#tags').removeClass('hidden'); 375 | var tagTable = []; 376 | for (var tag in tags) { 377 | tagTable.push([tag + ': ' + tags[tag], tags[tag]]); 378 | } 379 | tagTable.sort(function (a, b) { 380 | return b[1] - a[1]; 381 | }); 382 | tags = new google.visualization.DataTable(); 383 | tags.addColumn('string', 'Tag'); 384 | tags.addColumn('number', 'solved'); 385 | tags.addRows(tagTable); 386 | var tagOptions = { 387 | width: Math.max(600, $('#tags').width()), 388 | height: Math.max(600, $('#tags').width()) * 0.75, 389 | chartArea: { width: '80%', height: '70%' }, 390 | title: 'Tags of ' + handle, 391 | pieSliceText: 'none', 392 | legend: { 393 | position: 'right', 394 | alignment: 'center', 395 | textStyle: { 396 | fontSize: 12, 397 | fontName: 'Roboto' 398 | } 399 | }, 400 | pieHole: 0.5, 401 | tooltip: { 402 | text: 'percentage' 403 | }, 404 | fontName: 'Roboto', 405 | titleTextStyle: titleTextStyle, 406 | colors: colors.slice(0, Math.min(colors.length, tags.getNumberOfRows())) 407 | }; 408 | var tagChart = new google.visualization.PieChart(document.getElementById('tags')); 409 | tagChart.draw(tags, tagOptions); 410 | 411 | //Plotting levels 412 | $('#levels').removeClass('hidden'); 413 | var levelTable = []; 414 | for (var level in levels) { 415 | levelTable.push([level, levels[level]]); 416 | } 417 | levelTable.sort(function (a, b) { 418 | if (a[0] > b[0]) return -1; 419 | else return 1; 420 | }); 421 | levels = new google.visualization.DataTable(); 422 | levels.addColumn('string', 'Level'); 423 | levels.addColumn('number', 'solved'); 424 | levels.addRows(levelTable); 425 | var levelOptions = { 426 | width: Math.max($('#levels').width(), levels.getNumberOfRows() * 50), 427 | height: 300, 428 | title: 'Levels of ' + handle, 429 | legend: 'none', 430 | fontName: 'Roboto', 431 | titleTextStyle: titleTextStyle, 432 | vAxis: { format: '0' }, 433 | colors: ['#3F51B5'] 434 | }; 435 | var levelChart = new google.visualization.ColumnChart( 436 | document.getElementById('levels') 437 | ); 438 | if (levelTable.length > 1) levelChart.draw(levels, levelOptions); 439 | 440 | //Plotting ratings 441 | $('#ratings').removeClass('hidden'); 442 | var ratingTable = []; 443 | for (var rating in ratings) { 444 | ratingTable.push([rating, ratings[rating]]); 445 | } 446 | ratingTable.sort(function (a, b) { 447 | if (parseInt(a[0]) > parseInt(b[0])) return -1; 448 | else return 1; 449 | }); 450 | ratings = new google.visualization.DataTable(); 451 | ratings.addColumn('string', 'Rating'); 452 | ratings.addColumn('number', 'solved'); 453 | ratings.addRows(ratingTable); 454 | var ratingOptions = { 455 | width: Math.max($('#ratings').width(), ratings.getNumberOfRows() * 50), 456 | height: 300, 457 | title: 'Problem ratings of ' + handle, 458 | legend: 'none', 459 | fontName: 'Roboto', 460 | titleTextStyle: titleTextStyle, 461 | vAxis: { format: '0' }, 462 | colors: ['#3F51B5'] 463 | }; 464 | var ratingChart = new google.visualization.ColumnChart( 465 | document.getElementById('ratings') 466 | ); 467 | if (ratingTable.length > 1) ratingChart.draw(ratings, ratingOptions); 468 | 469 | /* heatmap */ 470 | $('#heatmapCon').removeClass('hidden'); 471 | $('#heatMapHandle').html(handle); 472 | var heatmapTable = []; 473 | for (var d in heatmap) { 474 | heatmapTable.push([new Date(parseInt(d)), heatmap[d]]); 475 | } 476 | heatmapData = new google.visualization.DataTable(); 477 | heatmapData.addColumn({ type: 'date', id: 'Date' }); 478 | heatmapData.addColumn({ type: 'number', id: 'Submissions' }); 479 | heatmapData.addRows(heatmapTable); 480 | 481 | heatmap = new google.visualization.Calendar(document.getElementById('heatmapDiv')); 482 | var heatmapOptions = { 483 | height: years * 140 + 30, 484 | width: Math.max($('#heatmapCon').width(), 900), 485 | fontName: 'Roboto', 486 | titleTextStyle: titleTextStyle, 487 | colorAxis: { 488 | minValue: 0, 489 | colors: ['#ffffff', '#0027ff', '#00127d'] 490 | }, 491 | calendar: { 492 | cellSize: 15 493 | } 494 | }; 495 | heatmap.draw(heatmapData, heatmapOptions); 496 | 497 | //parse all the solved problems and extract some numbers about the solved problems 498 | var tried = 0; 499 | var solved = 0; 500 | var maxAttempt = 0; 501 | var maxAttemptProblem = ''; 502 | var maxAc = ''; 503 | var maxAcProblem = ''; 504 | var unsolved = []; 505 | var solvedWithOneSub = 0; 506 | for (var p in problems) { 507 | tried++; 508 | if (problems[p].solved > 0) solved++; 509 | if (problems[p].solved === 0) unsolved.push(problems[p].problemlink); 510 | 511 | if (problems[p].attempts > maxAttempt) { 512 | maxAttempt = problems[p].attempts; 513 | maxAttemptProblem = problems[p].problemlink; 514 | } 515 | if (problems[p].solved > maxAc) { 516 | maxAc = problems[p].solved; 517 | maxAcProblem = problems[p].problemlink; 518 | } 519 | 520 | if (problems[p].solved > 0 && problems[p].attempts == 1) solvedWithOneSub++; 521 | } 522 | 523 | $('#numbers').removeClass('hidden'); 524 | $('#unsolvedCon').removeClass('hidden'); 525 | $('.handle-text').html(handle); 526 | $('#tried').html(tried); 527 | $('#solved').html(solved); 528 | $('#maxAttempt').html( 529 | maxAttempt + 530 | ' (' + 533 | maxAttemptProblem + 534 | ') ' 535 | ); 536 | if (maxAc > 1) 537 | $('#maxAc').html( 538 | maxAc + 539 | ' (' + 542 | maxAcProblem + 543 | ') ' 544 | ); 545 | else $('#maxAc').html(solved ? 1 : 0); 546 | $('#averageAttempt').html((totalSub / solved).toFixed(2)); 547 | $('#solvedWithOneSub').html( 548 | solvedWithOneSub + 549 | ' (' + 550 | (solved ? ((solvedWithOneSub / solved) * 100).toFixed(2) : 0) + 551 | '%)' 552 | ); 553 | 554 | unsolved.forEach(function (p) { 555 | var url = get_url(p); 556 | $('#unsolvedList').append( 557 | '
' + p + '
' 558 | ); 559 | }); 560 | } 561 | 562 | // reset all data 563 | function resetData() { 564 | // if the requests were already made, abort them 565 | if (req1) req1.abort(); 566 | if (req2) req2.abort(); 567 | verdicts = {}; 568 | langs = {}; 569 | tags = {}; 570 | levels = {}; 571 | problems = {}; 572 | totalSub = 0; 573 | heatmap = {}; 574 | ratings = {}; 575 | $('#mainSpinner').addClass('is-active'); 576 | $('.to-clear').empty(); 577 | $('.to-hide').addClass('hidden'); 578 | } 579 | 580 | // receives the problem id like 650-A 581 | // splits the contest id and problem index and returns the problem url 582 | function get_url(p) { 583 | var con = p.split('-')[0]; 584 | var index = p.split('-')[1]; 585 | 586 | var url = ''; 587 | if (con.length <= 4) 588 | url = 'https://codeforces.com/contest/' + con + '/problem/' + index; 589 | else url = 'https://codeforces.com/problemset/gymProblem/' + con + '/' + index; 590 | 591 | return url; 592 | } 593 | 594 | //Copied from stackoverflow :D gets url paramenter by name 595 | function getParameterByName(name, url) { 596 | if (!url) { 597 | url = window.location.href; 598 | } 599 | name = name.replace(/[\[\]]/g, '\\$&'); 600 | var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'), 601 | results = regex.exec(url); 602 | if (!results) return null; 603 | if (!results[2]) return ''; 604 | return decodeURIComponent(results[2].replace(/\+/g, ' ')); 605 | } 606 | 607 | // Opens a share window when the share button is clicked 608 | function fbShareResult() { 609 | var url = window.location.href + '?handle=' + handle; // generation share url 610 | var top = screen.height / 2 - 150; 611 | var left = screen.width / 2 - 300; 612 | window.open( 613 | 'https://facebook.com/sharer/sharer.php?u=' + escape(url), 614 | 'Share', 615 | 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=300,width=600,top=' + 616 | top + 617 | ',left=' + 618 | left 619 | ); 620 | } 621 | 622 | // shows am error message in the input form 623 | // Needs the div name of the input widget 624 | function err_message(div, msg) { 625 | $('#' + div + 'Err').html(msg); 626 | $('#' + div).addClass('is-invalid'); 627 | } 628 | -------------------------------------------------------------------------------- /js/vir.js: -------------------------------------------------------------------------------- 1 | // Virtual rating change 2 | 3 | var api_url = 'https://codeforces.com/api/'; 4 | var ratings = []; 5 | var places = []; 6 | var rows = []; 7 | var ratingsDict = {}; 8 | var handles = []; 9 | 10 | var contestId = -1; 11 | var points = -1; 12 | var rating = -1; 13 | var rank = -1; 14 | var penalty = -1; 15 | var userHandle = null; 16 | 17 | $(document).ready(function () { 18 | $('#inputform').submit(function (e) { 19 | $('#mainSpinner').addClass('is-active'); 20 | resetData(); 21 | e.preventDefault(); 22 | $('#rating').blur(); 23 | $('#points').blur(); 24 | $('#contestId').blur(); 25 | 26 | // the user may want to know rating change for the same contest for different points, rank, oldrating 27 | // we don't want to download the contest data again, as it takes really long 28 | // so we'll take a newContestId var, and check later if this is the same as the previously entered contestId 29 | var newContestId = $('#contestId').val().trim(); 30 | rating = $('#rating').val().trim(); 31 | points = $('#points').val().trim(); 32 | penalty = $('#penalty').val().trim(); 33 | userHandle = $('#handle').val().trim(); 34 | 35 | if (!newContestId) { 36 | err_message('contestIdDiv', 'Not valid contest ID'); 37 | return; 38 | } 39 | if (!points) { 40 | err_message('pointsDiv', 'Not valid points'); 41 | return; 42 | } 43 | if (!penalty) { 44 | err_message('penaltyDiv', 'Not valid penalty'); 45 | return; 46 | } 47 | if (!(rating || userHandle)) { 48 | err_message('ratingDiv', 'Rating must not be empty without user handle'); 49 | return; 50 | } 51 | 52 | if ( 53 | newContestId != contestId || 54 | rows.length == 0 || 55 | Object.keys(ratingsDict).length == 0 56 | ) { 57 | showMessage('Downloading data can take a few minutes. Thanks for your patience.'); 58 | contestId = newContestId; 59 | 60 | var req1 = $.get( 61 | api_url + 'contest.standings', 62 | { contestId: contestId }, 63 | function (data, status) { 64 | rows = data.result.rows; 65 | } 66 | ).fail(getDataFailed); 67 | 68 | // we need all the participants' ratings before the contest 69 | var req2 = $.get( 70 | api_url + 'contest.ratingChanges', 71 | { contestId: contestId }, 72 | function (data, status) { 73 | if (data.result.length == 0) { 74 | getDataFailed(); 75 | req1.abort(); 76 | return; 77 | } 78 | for (var i = 0; i < data.result.length; i++) { 79 | var change = data.result[i]; 80 | ratingsDict[change.handle] = change.oldRating; 81 | } 82 | } 83 | ).fail(getDataFailed); 84 | 85 | $.when(req1, req2).then(function () { 86 | if (Object.keys(ratingsDict).length != 0) { 87 | refresh(); 88 | } 89 | }); 90 | } else { 91 | setTimeout(refresh, 2); 92 | } 93 | }); 94 | }); 95 | 96 | function getDataFailed() { 97 | err_message( 98 | 'contestIdDiv', 99 | 'Contest not found, or not rated, or not finished yet, or bad network' 100 | ); 101 | } 102 | 103 | function refresh() { 104 | var handleFound = false; 105 | for (var i = 0; i < rows.length; i++) { 106 | // trying to guess what what would have been his rank if he participated in the real contest 107 | if ( 108 | (points > rows[i].points || 109 | (points == rows[i].points && penalty <= rows[i].penalty)) && 110 | rank == -1 111 | ) { 112 | handles.push('virtual user'); 113 | places.push(rows[i].rank); 114 | rank = rows[i].rank; 115 | } 116 | let currentHandle = rows[i].party.members[0].handle; 117 | if (userHandle == currentHandle) { 118 | handleFound = true; 119 | } else if (currentHandle) { 120 | places.push(rows[i].rank); 121 | handles.push(rows[i].party.members[0].handle); 122 | } 123 | } 124 | 125 | if (userHandle != '' && !handleFound) { 126 | err_message('handleDiv', 'User did not participate in contest'); 127 | return; 128 | } 129 | 130 | if (!rating) rating = ratingsDict[userHandle]; 131 | 132 | for (var i = 0; i < handles.length; i++) { 133 | ratings[i] = handles[i] in ratingsDict ? ratingsDict[handles[i]] : rating; 134 | } 135 | 136 | var results = CalculateRatingChanges(ratings, places, handles); 137 | showResult(results); 138 | } 139 | 140 | function resetData() { 141 | $('#mainSpinner').addClass('is-active'); 142 | $('#result').addClass('hidden'); 143 | ratings = []; 144 | places = []; 145 | handles = []; 146 | rank = -1; 147 | } 148 | 149 | function showResult(results) { 150 | $('#mainSpinner').removeClass('is-active'); 151 | $('#result').removeClass('hidden'); 152 | for (var i = 0; i < results.length; i++) { 153 | if (results[i].party == 'virtual user') { 154 | $('#change').html(results[i].delta > 0 ? '+' + results[i].delta : results[i].delta); 155 | $('#rank').html(rank); 156 | $('#position').html(parseInt(results[i].seed)); 157 | } 158 | } 159 | } 160 | 161 | function err_message(div, msg) { 162 | $('#mainSpinner').removeClass('is-active'); 163 | $('#' + div + 'Err').html(msg); 164 | $('#' + div).addClass('is-invalid'); 165 | } 166 | 167 | // 168 | function showMessage(text) { 169 | var data = { message: text, timeout: 10000 }; 170 | $('#loading-text')[0].MaterialSnackbar.showSnackbar(data); 171 | } 172 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "CFViz", 3 | "name": "Codeforces Visualizer", 4 | "icons": [ 5 | { 6 | "src": "/images/icon.png", 7 | "sizes": "512x512 192x192 64x64 32x32 24x24 16x16", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "/", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /styles/style.css: -------------------------------------------------------------------------------- 1 | /* Overriding default sidebar spacing */ 2 | 3 | main { 4 | margin-left: 0px !important; 5 | } 6 | 7 | header { 8 | margin-left: 0px !important; 9 | width: 100% !important; 10 | } 11 | 12 | #logo { 13 | font-size: 60px; 14 | margin: 40px auto 20px; 15 | } 16 | 17 | #mainSpinner { 18 | position: fixed; 19 | top: 0px; 20 | bottom: 0px; 21 | left: calc(50% - 14px); 22 | margin: auto; 23 | z-index: 10; 24 | } 25 | 26 | 27 | /* 28 | @media only screen and (min-width: 1025px) { 29 | #mainSpinner { 30 | left: calc(50% + 106px); 31 | } 32 | }*/ 33 | 34 | .handle-card { 35 | padding: 20px 50px; 36 | margin: auto; 37 | max-height: 250px; 38 | clear: both; 39 | text-align: center; 40 | } 41 | 42 | .card { 43 | margin: 10px auto; 44 | overflow-y: hidden; 45 | overflow-x: auto; 46 | background: #fff; 47 | } 48 | 49 | .card, 50 | .handle-card, 51 | .share-div, 52 | .fb-share-button { 53 | border-radius: 5px; 54 | } 55 | 56 | td { 57 | color: rgb(117, 117, 117); 58 | font-weight: 500; 59 | } 60 | 61 | table { 62 | width: 100%; 63 | border-radius: 5px; 64 | } 65 | 66 | a { 67 | text-decoration: none; 68 | } 69 | 70 | 71 | /* for footer */ 72 | 73 | .sharethis { 74 | text-transform: uppercase; 75 | font-weight: 600; 76 | color: white; 77 | padding: 6px 18px; 78 | border-radius: 5px; 79 | background-color: #4267b2; 80 | } 81 | 82 | .sharethis:hover { 83 | background-color: #365899; 84 | } 85 | 86 | .fb_iframe_widget_fluid { 87 | display: inline-block !important; 88 | } 89 | 90 | .fb-recommend { 91 | padding: 10px; 92 | } 93 | 94 | .sharethis, 95 | .fb-save, 96 | .fb-recommend { 97 | /*box-shadow: 1px 1px 2px black;*/ 98 | border-radius: 5px; 99 | } 100 | 101 | .share-div { 102 | text-align: center; 103 | /*background-color: #37474f;*/ 104 | min-height: 200px; 105 | margin-top: 20px; 106 | /*padding: 30px 0px;*/ 107 | } 108 | 109 | .vertical-space { 110 | width: 100%; 111 | height: 20px; 112 | } 113 | 114 | 115 | /*only for the about page*/ 116 | 117 | .fb-comments { 118 | width: 100%; 119 | } 120 | 121 | .comments-con { 122 | padding: 30px; 123 | background-color: white; 124 | border-radius: 5px; 125 | margin: auto; 126 | } 127 | 128 | .about { 129 | text-align: center; 130 | margin: auto; 131 | border-radius: 50%; 132 | background-color: white; 133 | padding: 40px; 134 | margin-bottom: 20px; 135 | } 136 | 137 | 138 | /* only for index */ 139 | 140 | #unsolvedCon { 141 | padding: 30px 20px 50px 50px; 142 | background-color: white; 143 | } 144 | 145 | #unsolvedTitle, #commonContestTitle { 146 | color: #393939; 147 | font-weight: 500; 148 | font-size: 20px; 149 | margin: -20px; 150 | } 151 | 152 | #heatmapCon { 153 | padding: 30px; 154 | } 155 | 156 | #heatmapTitle { 157 | font-size: 20px; 158 | color: #393939; 159 | } 160 | 161 | #heatmapMaxValue { 162 | text-align: center; 163 | } 164 | 165 | .heatmap-text { 166 | font-size: 12px; 167 | text-align: center; 168 | width: 155px; 169 | } 170 | 171 | .lnk { 172 | margin: 2px 5px; 173 | float: left; 174 | } 175 | 176 | 177 | /* only for compare */ 178 | 179 | #submitButton { 180 | margin: auto; 181 | } 182 | 183 | .handle1Color { 184 | color: #009688; 185 | } 186 | .handle2Color { 187 | color: #3F51B5; 188 | } 189 | 190 | #commonSolvedTable { 191 | width: 250px; 192 | } 193 | 194 | 195 | /* only for virtual */ 196 | #info { 197 | padding: 40px; 198 | } 199 | 200 | .input-card { 201 | padding: 20px 50px; 202 | margin: auto; 203 | clear: both; 204 | text-align: center; 205 | } 206 | 207 | 208 | /* scrollbar (only for chrome and safari) */ 209 | 210 | ::-webkit-scrollbar { 211 | height: 5px; 212 | width: 5px; 213 | } 214 | 215 | ::-webkit-scrollbar-track { 216 | display: none; 217 | } 218 | 219 | ::-webkit-scrollbar-thumb { 220 | border-radius: 5px; 221 | background-color: #607D8B; 222 | } 223 | 224 | 225 | /*things I don't know much about*/ 226 | 227 | html, 228 | body { 229 | font-family: 'Roboto', 'Helvetica', sans-serif; 230 | } 231 | 232 | .layout .mdl-layout__header .mdl-layout__drawer-button { 233 | color: rgba(0, 0, 0, 0.54); 234 | } 235 | 236 | .mdl-layout__drawer .avatar { 237 | margin-bottom: 16px; 238 | } 239 | 240 | .drawer { 241 | border: none; 242 | } 243 | 244 | 245 | /* iOS Safari specific workaround */ 246 | 247 | .drawer .mdl-menu__container { 248 | z-index: -1; 249 | } 250 | 251 | .drawer .navigation { 252 | z-index: -2; 253 | } 254 | 255 | 256 | /* END iOS Safari specific workaround */ 257 | 258 | .drawer .mdl-menu .mdl-menu__item { 259 | display: -webkit-flex; 260 | display: -ms-flexbox; 261 | display: flex; 262 | -webkit-align-items: center; 263 | -ms-flex-align: center; 264 | align-items: center; 265 | } 266 | 267 | .drawer-header { 268 | box-sizing: border-box; 269 | display: -webkit-flex; 270 | display: -ms-flexbox; 271 | display: flex; 272 | -webkit-flex-direction: column; 273 | -ms-flex-direction: column; 274 | flex-direction: column; 275 | -webkit-justify-content: flex-end; 276 | -ms-flex-pack: end; 277 | justify-content: flex-end; 278 | padding: 16px; 279 | height: 151px; 280 | } 281 | 282 | .navigation { 283 | -webkit-flex-grow: 1; 284 | -ms-flex-positive: 1; 285 | flex-grow: 1; 286 | } 287 | 288 | .layout .navigation .mdl-navigation__link { 289 | display: -webkit-flex !important; 290 | display: -ms-flexbox !important; 291 | display: flex !important; 292 | -webkit-flex-direction: row; 293 | -ms-flex-direction: row; 294 | flex-direction: row; 295 | -webkit-align-items: center; 296 | -ms-flex-align: center; 297 | align-items: center; 298 | color: #424242; 299 | font-weight: 500; 300 | } 301 | 302 | .layout .navigation .mdl-navigation__link:hover { 303 | background-color: #00BCD4; 304 | color: #37474F; 305 | } 306 | 307 | .navigation .mdl-navigation__link .material-icons { 308 | font-size: 24px; 309 | color: rgba(255, 255, 255, 0.56); 310 | margin-right: 32px; 311 | } 312 | 313 | .content { 314 | max-width: 1080px; 315 | } 316 | -------------------------------------------------------------------------------- /sw.js: -------------------------------------------------------------------------------- 1 | importScripts( 2 | 'https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js' 3 | ); 4 | 5 | workbox.precaching.precacheAndRoute([ 6 | { url: '/index.html', revision: '1119' }, 7 | { url: '/about.html', revision: '1115' }, 8 | { url: '/compare.html', revision: '11154' }, 9 | { url: '/virtual-rating-change.html', revision: '1117' }, 10 | { url: '/js/compare_helper.js', revision: '1115' }, 11 | { url: '/js/compare.js', revision: '1112' }, 12 | { url: '/js/calculate.js', revision: '1113' }, 13 | { url: '/js/single.js', revision: '1114' }, 14 | { url: '/js/vir.js', revision: '1115' }, 15 | ]); 16 | 17 | workbox.routing.registerRoute( 18 | /^((?!codeforces)(?!facebook)(?!analytics)(?!ads)(?!google).)*$/, 19 | new workbox.strategies.StaleWhileRevalidate({ 20 | cacheName: 'local', 21 | }) 22 | ); 23 | -------------------------------------------------------------------------------- /virtual-rating-change.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Codeforces Visualizer | Virtual Rating Change Calculator 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 53 | 94 | 95 | 96 | 97 |
98 |
99 |
100 |
101 | Codeforces Visualizer 102 |
103 | 109 |
110 |
111 |
112 | 113 | 119 |
120 |
121 |
122 | 123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 | 131 | 132 | Not valid contestId 133 |
134 |
135 | 136 | 137 | Not valid points 138 |
139 |
140 | 141 | 142 | 143 |
144 |
145 | 146 | 147 | Not valid penalty 148 |
149 |
150 | 151 | 152 | Not valid rating 153 |
154 |
155 | 156 |
157 |
158 | 182 |
183 |

Have you ever wondered if you could know what would have been your rating change if you participated in a contest live rather than virtual? Or if you could solve one more problem in the last contest?

184 |

Well, now you can. Just enter the contest id, points gained in the contest and rating.

185 |

Note: Contest id is not the round number. It is the id that appears in contest url. Like codeforces.com/contest/577/

186 |

187 | 198 |
199 |
200 |
201 | 202 | 203 | 204 | 205 | --------------------------------------------------------------------------------