├── .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 |
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 |
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 |
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 | ''
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 |
201 |
202 |
203 |
204 |
205 |
--------------------------------------------------------------------------------