├── .gitignore
├── LICENSE.txt
├── builder.js
├── docs
├── atcoder_custom_standings.user.js
├── index.html
├── modal.user.js
└── ranking_script.user.js
├── img
├── img1.png
└── img2.png
├── package.json
├── readme.md
├── readme_eng.md
├── release_notes.md
├── src
├── app.js
├── appSettings.js
├── contestData.js
├── controll.js
├── css.js
├── filter.js
├── friendsList.js
├── main.js
├── modal.js
├── pager.js
├── reload.js
├── settings.js
├── sorting.js
├── standings.js
├── stats.js
├── stats
│ ├── chartComponent.js
│ ├── summary.js
│ └── task.js
├── userinfo.js
└── util.js
├── test
└── main.js
└── userscript_header.js
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 | standings
3 | standings.html
4 | Standings.htm
5 | /Standings_files/
6 | standings.js
7 | stats.html
8 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright 2017 koyumeishi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/builder.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 |
3 | const head = fs.readFileSync( 'userscript_header.js', {encoding:'utf-8'})
4 | .replace(/__version__/g, process.argv[2]);
5 |
6 | const content = fs.readFileSync( 'test/main.js', {encoding:'utf-8'});
7 |
8 | console.log("building version : ", process.argv[2]);
9 |
10 | fs.writeFileSync( 'docs/atcoder_custom_standings.user.js', `${head}
11 |
12 | ${content}`);
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AtCoder Scripts
6 |
7 |
8 | https://github.com/koyumeishi/atcoder_script
9 | atcoder_custom_standings.user.js
10 |
11 |
12 |
--------------------------------------------------------------------------------
/docs/modal.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name AtCoderModalMoudarui
3 | // @namespace koyumeishi_scripts_AtCoderModalMoudarui
4 | // @downloadURL https://koyumeishi.github.io/atcoder_script/modal.user.js
5 | // @version 0.02
6 | // @description modal moudarui
7 | // @author koyumeishi
8 | // @match http://*.contest.atcoder.jp/*
9 | // @match https://*.contest.atcoder.jp/*
10 | // @grant none
11 | // ==/UserScript==
12 |
13 | (function(){
14 | document.getElementById('modal-contest-begin').setAttribute('id', 'modal-contest-begin-disabled');
15 | document.getElementById('modal-contest-end').setAttribute('id', 'modal-contest-end-disabled');
16 | })();
--------------------------------------------------------------------------------
/img/img1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koyumeishi/atcoder_script/4a0db586bc4fd015544442520ed2d9ccfc2934a8/img/img1.png
--------------------------------------------------------------------------------
/img/img2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/koyumeishi/atcoder_script/4a0db586bc4fd015544442520ed2d9ccfc2934a8/img/img2.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "atcodercustomstandings",
3 | "version": "1.0.5",
4 | "description": "atcodercustomstandings",
5 | "dependencies": {
6 | "react": "^15.4.2",
7 | "react-dom": "^15.4.2"
8 | },
9 | "devDependencies": {
10 | "babel-cli": "^6.23.0",
11 | "babel-preset-es2015": "^6.22.0",
12 | "babel-preset-react": "^6.23.0",
13 | "babelify": "^7.3.0",
14 | "chart.js": "^2.5.0"
15 | },
16 | "scripts": {
17 | "build": "browserify src\\main.js -t babelify -o test\\main.js -d",
18 | "build:release": "browserify src\\main.js -t babelify -o test\\main.js && node builder.js %npm_package_version%"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/koyumeishi/atcoder_script.git"
23 | },
24 | "author": "koyumeishi",
25 | "bugs": {
26 | "url": "https://github.com/koyumeishi/atcoder_script/issues"
27 | },
28 | "homepage": "https://github.com/koyumeishi/atcoder_script",
29 | "babel": {
30 | "presets": [
31 | "es2015",
32 | "react"
33 | ]
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # AtCoder Scripts
2 | [@koyumeishi_](https://twitter.com/koyumeishi_) の AtCoder用 UserScript置き場
3 |
4 |
5 | ##目録
6 | * [AtCoderCustomStandings](#AtCoderCustomStandings)
7 | * [AtCoderModalMoudarui](#AtCoderModalMoudarui)
8 |
9 | ## AtCoderCustomStandings
10 | [https://koyumeishi.github.io/atcoder_script/atcoder_custom_standings.user.js](https://koyumeishi.github.io/atcoder_script/atcoder_custom_standings.user.js)
11 | #### English version is [here](readme_eng.md)
12 |
13 | * [Introduction](#introduction)
14 | * [Installation](#install)
15 | * [Usage](#usage)
16 | * [Release Notes](release_notes.md)
17 | * [License](#license)
18 |
19 | ---
20 |
21 | ### Introduction
22 | AtCoderの順位表をカスタマイズして表示する非公式UserScriptです。 FirefoxのGreasemonkeyとGoogleChromeのTampermonkeyで動作確認しています。 javascript素人が書いたので予期せぬバグや、AtCoder側の仕様変更により使えなくなる場合があるかも知れません。 大切なコンテストでの使用は自己責任でお願いします。
23 | 他の順位表のページをいじる拡張機能やuserscriptとは競合する場合があります。
24 |
25 | 主な機能
26 |
27 | * friendだけの順位表を表示
28 | * 1ページ当たりの表示件数変更(大量に表示すると重いです)
29 | * 順位表自動更新
30 | * 国、レーティング、名前によるフィルター
31 | * 統計情報の表示(順位表から読み取れる分だけなので公式のstatsとは差異が生じます)
32 |
33 | ### Installation
34 |
35 | 1. Greasemonkey(Firefox)やTampermonkey(GoogleChrome)やらをブラウザにインストールしておく
36 | 2. 次のリンクを開くとインストールするか尋ねられると思います
37 | [https://koyumeishi.github.io/atcoder_script/atcoder_custom_standings.user.js](https://koyumeishi.github.io/atcoder_script/atcoder_custom_standings.user.js)
38 |
39 | ### Usage
40 | http://*.contest.atcoder.jp/standings*
41 | にアクセスすると、カスタマイズされた順位表が表示されます。
42 |
43 | > 
44 |
45 | ---
46 | Stats
47 | > 
48 |
49 | friendの登録/解除は、順位表のユーザー名をクリックして出てきたメニュー、または Settings から設定可能です。
50 | friends list等の設定はブラウザに保存されます。
51 |
52 | ### License
53 | MIT
54 |
55 |
56 | ## AtCoderModalMoudarui
57 | [https://koyumeishi.github.io/atcoder_script/modal.user.js](https://koyumeishi.github.io/atcoder_script/modal.user.js)
58 |
59 |
60 |
61 |
62 | って人向けにAtCoderのコンテスト開始時/終了時に表示されるモーダルを非表示にするscriptも書いてみました。
63 | ~~実際のコンテスト時に非表示になるか未確認ですが。~~
64 | 確認しましたが、コンテストの開始・終了がハッキリしないので使用感微妙だと思います。個人的には非推奨です。
65 |
66 |
67 |
--------------------------------------------------------------------------------
/readme_eng.md:
--------------------------------------------------------------------------------
1 | # AtCoderCustomStandings
2 | UserScript to customize your standings page of [AtCoder](https://atcoder.jp/?lang=en).
3 | [https://koyumeishi.github.io/atcoder_script/atcoder_custom_standings.user.js](https://koyumeishi.github.io/atcoder_script/atcoder_custom_standings.user.js)
4 |
5 | #### Japanese version is [here](readme.md)
6 |
7 | * [Features](#Features)
8 | * [Installation](#install)
9 | * [Usage](#usage)
10 | * [Release Notes](release_notes.md)
11 | * [License](#license)
12 |
13 | ---
14 |
15 | ## Features
16 |
17 | * Friends Standings
18 | * Modify displaying number of people per page
19 | * Auto reloading
20 | * Filtering by country, rating and user name
21 | * Statistics
22 |
23 | ## Installation
24 |
25 | 1. Install Greasemonkey(Firefox) or Tampermonkey(GoogleChrome) on your browser (if you haven't installed)
26 | 2. Open the link below and install the script
27 | [https://koyumeishi.github.io/atcoder_script/atcoder_custom_standings.user.js](https://koyumeishi.github.io/atcoder_script/atcoder_custom_standings.user.js)
28 |
29 | ## Usage
30 | This script runs at
31 |
32 | http://*.contest.atcoder.jp/standings*
33 |
34 | * Filtering
35 | > 
36 |
37 | ---
38 | * Statistics
39 | > 
40 |
41 | * Add / Remove friends
42 | * open user detail menu clicking the user name of standings table, and then click "Add to Friends List" / "Remove from Friends List"
43 | * open settings and type the user name and click "Add Friends" button / select friens and click "Remove Friends".
44 |
45 | All of the settings (including Friends List) are saved on your browser.
46 |
47 | ## License
48 | MIT
49 |
50 |
--------------------------------------------------------------------------------
/release_notes.md:
--------------------------------------------------------------------------------
1 | ### Release Notes
2 |
3 | ##### v1.0.4 2017.07.02
4 | - user メニューに twitter id、 コンテスト参加回数 追加
5 |
6 | ##### v1.0.4 2017.06.26
7 | - AC数 0 のときの統計グラフの挙動修正。 include に https 追加
8 |
9 | ##### v1.0.3 2017.06.25
10 | - Friends ボタン追加。 フィルタリングの状態を保持するオプションを追加
11 |
12 | ##### v1.0.2 2017.06.25
13 | - Summary に表示する情報追加
14 |
15 | ##### v1.0.1 2017.06.22
16 | - 誤字訂正 Summery -> Summary
17 |
18 | ##### v1.0.0 2017.06.19
19 | - React.js に書き換え。 フィルタ機能、統計機能等の追加。
20 |
21 | ##### v0.25 2016.10.10
22 | - coutry filter の先頭に表示する国を自国 -> 二番目に表示するよう変更
23 |
24 | ##### v0.24 2016.10.10
25 | - Your Rank の表示がリロード時に壊れてたのを修正
26 |
27 | ##### v0.23 2016.10.02
28 | - country filter の先頭に表示する国を自国へ変更
29 | - country filter ON時に自分の順位へ移動できなかったバグを解消
30 |
31 | ##### v0.22 2016.09.17
32 | - country filter に表示するを参加者のいる国に限定
33 |
34 | ##### v0.21 2016.09.15
35 | - atcoderの仕様変更に伴う改善他
36 | 1. user_id のみ表示されるようになったのでユーザー名表示オプションは廃止
37 | 1. 順位表更新時に自分の順位の位置にページ切り替えする仕様廃止
38 | 1. national flag に対応
39 | 1. 公式にテーブルヘッダのリンク先が問題ページになったのでこれの切り替えも廃止
40 | 1. 順位表更新時に取得したjson文字列を邪悪な方法で更新してたのでちゃんとjsonとしてパースするよう変更
41 | 1. コンテスト終了後、各問題/各ユーザーのsubmissionを確認できるリンクを追加
42 | 1. 国別フィルターの実装
43 |
44 | ##### v0.20 2016.07.18
45 | - new.atcoder.jp に対応
46 |
47 | ##### v0.19 2016.07.10
48 | - ARC057のレート更新
49 |
50 | ##### v0.18 2016.06.27
51 | - ARC056のレート更新
52 |
53 | ##### v0.17 2016.06.04
54 | - ARC055のレート更新
55 |
56 | ##### v0.16 2016.05.22
57 | - ARC054のレート更新
58 |
59 | ##### v0.15 2016.05.14
60 | - ARC053のレート更新
61 |
62 | ##### v0.14 2016.05.01
63 | - ARC052のレート更新
64 |
65 | ##### v0.13 2016.04.16
66 | - ARC051のレート更新
67 |
68 | ##### v0.12 2016.04.03
69 | - ARC050のレート更新
70 |
71 | ##### v0.11 2016.03.19
72 | - ARC049のレート更新
73 |
74 | ##### v0.10 2016.03.05
75 | - ARC048のレート更新
76 |
77 | ##### v0.09 2016.02.29
78 | - ユーザー名 / AtCoderID, AtCoderID / ユーザー名 の表示方式を追加
79 | - friend listに登録された人を強調表示する機能の ON/OFF を追加
80 |
81 | ##### v0.08 2016.02.13
82 | - 順位表上部の問題名のリンク先を変更したとき、target="_blank"に変更
83 | - ページ下部にAtCoderCustomStandings/ratingのバージョンを表示
84 |
85 | ##### v0.07 2016.01.17
86 | - ARC047のレート更新
87 | - 手動で"順位更新"をクリックしたときに自分の順位が正しく表示されない不具合を修正
88 | - 順位表上部の問題名のリンク先を変更する機能を実装
89 |
90 | ##### v0.06 2016.01.12
91 | - 非同期通信を理解していなかったので修正
92 |
93 | ##### v0.05 2016.01.10
94 | - 順位表の凍結に対応(仮)
95 | - 現在順位の表示、自分の位置までスクロールする機能を追加
96 | - ページ再読み込みなしでの順位表更新機能追加(ajaxでstandingsのページを取得してるので実質的には再読み込みしてる)
97 | - 順位表自動更新機能追加
98 | - 1ページ当たりの表示件数に"500件表示"を追加
99 | - rating色付け周りでコードがおかしかったのを修正
100 |
101 | ##### v0.04 2015.12.14
102 | - 星のemojiは環境次第で着色不可っぽいのでFriend Listに入っていないときはiconを表示するように戻した
103 |
104 | ##### v0.03 2015.12.14
105 | - ARC046のレート更新
106 | - 「Friend Listに登録/解除」オンマウス時のマウスカーソル変更、星のiconをemojiに変更
107 | - ユーザー名にhtmlコードを使っているとjQueryが拾ってしまう脆弱性を解消
108 | - ドロップダウンメニューにレーティング情報追加
109 |
110 | ##### v0.02 2015.11.09
111 | - 正の得点をしていない提出の提出時間が 00:00 になっていたのを修正(時間を非表示に)
112 |
113 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | import * as util from './util.js';
2 | import AppSettings from './appSettings.js';
3 | import FriendsList from './friendsList.js';
4 | import ContestData from './contestData.js';
5 | import Stats from './stats.js';
6 | import Controll from './controll.js';
7 | import Standings from './standings.js';
8 | import Pager from './pager.js';
9 | import Me from './userinfo.js';
10 |
11 | export default class AtCoderCustomStandings extends React.Component {
12 | constructor(){
13 | super();
14 | this.state = {};
15 | this.state.settings = new AppSettings( true );
16 | this.state.friends = new FriendsList( true );
17 |
18 | util.getStandings( (s) => {
19 | this.standings = s;
20 | } , true);
21 |
22 | this.contest = new ContestData();
23 |
24 | this.state.filteredStandings = this.getFilteredStandings( this.state.settings );
25 | this.state.currentPage = 0; //zero-indexed
26 | this.state.totalPage = Math.max(1, Math.floor( (this.state.filteredStandings.length + this.state.settings.pageSize - 1) / this.state.settings.pageSize ) );
27 |
28 | this.getFilteredStandings.bind(this);
29 | this.getFilteredStandingsToRender.bind(this);
30 | this.updateStandings.bind(this);
31 |
32 | this.updateFriends.bind(this);
33 | this.updateSettings.bind(this);
34 | }
35 |
36 | updateSettings( newSettings ){
37 | newSettings.save();
38 | this.setState( (prevState) => {
39 | const newFilteredStandings = this.getFilteredStandings( newSettings );
40 | const totalPage = Math.max(1, Math.floor( (newFilteredStandings.length + newSettings.pageSize - 1) / newSettings.pageSize ) );
41 | const currentPage = Math.min(totalPage-1, prevState.currentPage);
42 |
43 | return {
44 | settings : newSettings,
45 | filteredStandings : newFilteredStandings,
46 | totalPage : totalPage,
47 | currentPage : currentPage
48 | };
49 | });
50 | }
51 |
52 | updateFriends( handleNames, adding ){
53 | this.setState( (prevState) => {
54 | let newFriends = new FriendsList( false );
55 | newFriends.friends = new Set( prevState.friends.getList() );
56 | if( adding === true ){
57 | newFriends.add(handleNames);
58 | }else if( adding === false ){
59 | newFriends.remove(handleNames);
60 | }
61 | return { friends : newFriends };
62 | } );
63 | }
64 |
65 | updateStandings(){
66 | console.log("started updating");
67 |
68 | util.getStandings( (s) => {
69 | this.standings = s;
70 | this.setState( (prevState) => {
71 | const newFilteredStandings = this.getFilteredStandings( this.state.settings );
72 | const totalPage = Math.max(1, Math.floor( (newFilteredStandings.length + this.state.settings.pageSize - 1) / this.state.settings.pageSize ) );
73 | const currentPage = Math.min(totalPage-1, prevState.currentPage);
74 |
75 | return {
76 | filteredStandings : newFilteredStandings,
77 | totalPage : totalPage,
78 | currentPage : currentPage
79 | };
80 | } );
81 | console.log( "standings updating successfully completed" );
82 | } , false);
83 | }
84 |
85 |
86 | getFilteredStandings(settings){
87 | const r = util.rating;
88 | let nameReg;
89 | try{
90 | nameReg = new RegExp( "^" + settings.filterName , "i");
91 | }catch(e){
92 | nameReg = new RegExp( "" );
93 | }
94 | let fStandings = this.standings.filter( row => {
95 | if(settings.filterByFriends === true){
96 | if(this.state.friends.isFriend( row.user_screen_name ) === false &&
97 | row.user_screen_name !== Me.user_screen_name){
98 | return false;
99 | }
100 | }
101 | if(settings.filterByCountry === true){
102 | if( row.country !== settings.filterCountry ){
103 | return false;
104 | }
105 | }
106 | if(settings.filterByRating === true){
107 | // rating filter function
108 | // row.rating
109 | const level = r.getLevel( row.rating );
110 | if( settings.filterRating.has(level) === false ){
111 | return false;
112 | }
113 | }
114 | if(settings.filterByName === true){
115 | if( nameReg.exec( row.user_screen_name ) === null && nameReg.exec( row.user_name ) === null ){
116 | return false;
117 | }
118 | }
119 | return true;
120 | } );
121 |
122 | if( settings.sortingEnabled === true ){
123 | let f = util.getSortingFunction( settings.sortingKey );
124 | if( settings.sortingOrder === "ascending") return fStandings.sort( f );
125 | else return fStandings.sort( (a,b)=>f(a,b)*-1 );
126 | }else{
127 | return fStandings;
128 | }
129 | }
130 |
131 | getFilteredStandingsToRender(){
132 | const pageBegin = this.state.settings.pageSize * this.state.currentPage;
133 | const pageEnd = this.state.settings.pageSize * (this.state.currentPage+1);
134 | return this.state.filteredStandings.slice( pageBegin, pageEnd );
135 | }
136 |
137 | render(){
138 | const pageMe = (()=>{
139 | const pos = this.state.filteredStandings.findIndex( (row)=>{return row.user_screen_name === Me.user_screen_name} );
140 | if( pos === -1 ) return null;
141 | return Math.floor( pos/this.state.settings.pageSize );
142 | })();
143 | let s = this.getFilteredStandingsToRender();
144 | let components = (
145 |
146 |
this.updateStandings()}
148 | contest={this.contest}
149 | settings={this.state.settings}
150 | settingsUpdateFunc={ (newSettings)=>{
151 | this.updateSettings(newSettings);
152 | }}
153 | friends={this.state.friends}
154 | friendsUpdateFunc={(name, adding)=>this.updateFriends(name,adding)}
155 | getActiveCountries={()=>{
156 | return [...(new Set( this.standings.map( (e)=>e.country ) ))].sort( (a,b)=> {return util.countries[a] < util.countries[b] ? -1 : 1;} );
157 | }}/>
158 | {
161 | const page = Number( e.target.getAttribute('data-page') );
162 | this.setState( {currentPage : page} );
163 | } }/>
164 | this.updateFriends(name,adding)}
170 | offSet={this.state.currentPage*this.state.settings.pageSize}/>
171 | {
174 | e.preventDefault();
175 | const page = Number( e.target.getAttribute('data-page') );
176 | this.setState( {currentPage : page} );
177 | } }/>
178 |
179 | );
180 | return components;
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/src/appSettings.js:
--------------------------------------------------------------------------------
1 | export default class AppSettings{
2 | constructor( load ){
3 | //options
4 | this.highlightFriends = true;
5 | this.disableRatingColor = false;
6 | this.displayNameStyle = "user_screen_name";
7 | this.pageSize = 50;
8 | this.showNationalFlag = true;
9 |
10 | this.saveFilteringState = false;
11 |
12 | this.filterCountry = null;
13 | this.filterRating = new Set([1,2,3,4,5,6,7,8,9]);
14 |
15 | this.filterByFriends = false;
16 | this.filterByCountry = false;
17 | this.filterByRating = false;
18 | this.filterByName = false;
19 | this.filterName = "";
20 |
21 | if(load === true) this.load();
22 |
23 | if( this.saveFilteringState === false ){
24 | //reset temporary options
25 | this.filterByFriends = false;
26 | this.filterByCountry = false;
27 | this.filterByRating = false;
28 | this.filterByName = false;
29 | this.filterName = "";
30 | }
31 |
32 | this.sortingEnabled = false;
33 | // "rank", "user_screen_name", "rating", "country", "competitions", "task{i}"
34 | this.sortingKey = "rank";
35 | this.sortingOrder = "ascending";
36 |
37 | this.load.bind(this);
38 | this.save.bind(this);
39 | }
40 |
41 | load(){
42 | //load
43 | try{
44 | const settings = JSON.parse( GM_getValue('settings', '{}') );
45 | Object.assign( this, settings);
46 | if( this.filterRating === undefined) this.filterRating = new Set([1,2,3,4,5,6,7,8,9]);
47 | else this.filterRating = new Set(this.filterRating);
48 |
49 | console.log("loaded : settings");
50 | console.log(this);
51 | }catch(e){
52 | console.log("faild to load settings");
53 | console.log(e);
54 | }
55 | }
56 | save(){
57 | //save
58 | this.filterRating = [...this.filterRating];
59 |
60 | const settings = Object.assign({}, this);
61 | const str = JSON.stringify( settings );
62 |
63 | this.filterRating = new Set(this.filterRating);
64 |
65 | GM_setValue('settings', str);
66 |
67 | console.log("saved : settings");
68 | console.log(str);
69 | }
70 |
71 | isFiltersEnabled(){
72 | return this.filterByFriends || this.filterByCountry || this.filterByRating || this.filterByName;
73 | }
74 | }
--------------------------------------------------------------------------------
/src/contestData.js:
--------------------------------------------------------------------------------
1 | export default class ContestData{
2 | constructor(){
3 | this.contstName = $("div.container > a.brand > span.contest-name").text();
4 | this.startTime = new Date( Date.parse($('time#contest-start-time').text()) );
5 | this.endTime = new Date( Date.parse($('time#contest-end-time').text()) );
6 |
7 | this.contestEnded = (new Date()) >= this.endTime;
8 |
9 | const thead = $('#contest-standings > thead > tr > th');
10 | this.numTasks = thead.length - 3;
11 | this.tasks = new Array( this.numTasks );
12 | for(let i=0; i
28 | people
29 | Friends
30 |
31 | );
32 |
33 | return (
34 |
35 |
{
36 | let newSettings = Object.assign(new AppSettings(), this.props.settings);
37 | newSettings["filterByFriends"] = !this.props.settings.filterByFriends;
38 | this.props.settingsUpdateFunc( newSettings );
39 | } }>{button}
40 |
41 | );
42 | }
43 | }
44 |
45 | export default class Controll extends React.Component {
46 | constructor(props){
47 | super(props);
48 | }
49 |
50 | render(){
51 | let ret = (
52 |
53 |
54 |
57 |
58 |
59 |
63 |
64 |
65 |
70 |
71 |
72 |
77 |
78 |
79 |
83 |
84 |
85 |
91 |
92 |
93 | );
94 |
95 | return ret;
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/css.js:
--------------------------------------------------------------------------------
1 | export default function injectCustomCSS(){
2 | let css = `
3 | /* Rules for sizing the icon. */
4 | .material-icons.md-18 { font-size: 18px; }
5 | .material-icons.md-24 { font-size: 24px; }
6 | .material-icons.md-36 { font-size: 36px; }
7 | .material-icons.md-48 { font-size: 48px; }
8 |
9 | /* Rules for using icons as black on a light background. */
10 | .material-icons.md-dark { color: rgba(0, 0, 0, 0.54); }
11 | .material-icons.md-dark.md-inactive { color: rgba(0, 0, 0, 0.26); }
12 |
13 | /* Rules for using icons as white on a dark background. */
14 | .material-icons.md-light { color: rgba(255, 255, 255, 1); }
15 | .material-icons.md-light.md-inactive { color: rgba(255, 255, 255, 0.3); }
16 |
17 | /* Controller Button */
18 | .atcoder-custom-standings.controller-button {
19 | }
20 | .atcoder-custom-standings.controller-button:hover {
21 | background-color: rgba(220,220,220,0.1);
22 | box-shadow:2px 4px 8px 0px grey;
23 | cursor:pointer;
24 | text-decoration: underline;
25 | }
26 |
27 | /* Modal */
28 | .atcoder-custom-standings.modal-filter{
29 | position : fixed;
30 | top : 0;
31 | left : 0;
32 | width : 100%;
33 | height : 100%;
34 | padding-top : 50px;
35 | background-color : rgba(0,0,0,0.5);
36 | }
37 | .atcoder-custom-standings.modal-content{
38 | position: fixed;
39 | top :50%;
40 | left: 50%;
41 | z-index:1050;
42 | overflow:auto;
43 | background-color:white;
44 | box-shadow:0 3px 8px 3px rgba(0,0,0,0.3);
45 | width : 850px;
46 | height : 600px;
47 | max-height : 600px;
48 | margin : -300px 0 0 -425px;
49 | padding: 25px;
50 | }
51 |
52 | /* Check Box */
53 | .material-icons.md-checked { color : rgba(0, 122, 20, 0.9); }
54 |
55 | /* Reloading On Off*/
56 | .atcoder-custom-standings.reloading-enabled { color: rgb(230, 128, 63); }
57 | .atcoder-custom-standings.reloading-disabled { color: grey; }
58 |
59 | /* Sorting On Off*/
60 | .atcoder-custom-standings.sorting-enabled { color: rgb(230, 128, 63); }
61 | .atcoder-custom-standings.sorting-disabled { color: grey; }
62 |
63 | /* Filter On Off*/
64 | .atcoder-custom-standings.filtering-enabled { color: rgb(230, 128, 63); }
65 | .atcoder-custom-standings.filtering-disabled { color: grey; }
66 |
67 | /* Settings Item */
68 | .atcoder-custom-standings.settings-item {
69 | padding: 4px;
70 | display: block;
71 | }
72 |
73 | /* Standings table */
74 | .atcoder-custom-standings.timestamp { color:grey; display: block; }
75 |
76 | /* Other */
77 | .atcoder-custom-standings.cursor-link:hover{
78 | cursor: pointer;
79 | text-decoration: underline;
80 | }
81 |
82 | /* Standings pop down menu */
83 | .atcoder-custom-standings.user-dropdown-menu-box {
84 | position:absolute;
85 | padding-top:8px;
86 | padding-bottom:8px;
87 | background-color:white;
88 | box-shadow:4px 4px 8px 4px grey;
89 | border-radius:0px 0px 6px 0px;
90 | cursor: auto;
91 | }
92 | .atcoder-custom-standings.user-dropdown-menu {
93 | display : block;
94 | line-height: 2em;
95 | padding-left : 8px;
96 | padding-right : 8px;
97 | }
98 | .atcoder-custom-standings.user-dropdown-menu:hover {
99 | background : lightblue;
100 | }
101 |
102 | /* modify original */
103 | a.user-red {
104 | color:#FF0000;
105 | }
106 |
107 | .standings-friend td {background-color : rgba(0, 150, 100, 0.09) !important;}
108 | .standings-friend:hover td {background-color: rgba(0, 200, 150, 0.09) !important;}
109 |
110 | .standings-friend > td.standings-frozen {background-color : rgba(0, 82, 255, 0.27) !important;}
111 | .standings-friend > td.standings-frozen:hover {background-color: rgba(0, 82, 255, 0.27) !important;}
112 |
113 |
114 | .table-striped tbody tr:nth-child(odd) td, .table-striped tbody tr:nth-child(odd) th {background-color: #fefefe;}
115 | .table tbody tr:hover td, .table tbody tr:hover th {background-color: #fefefe;}
116 |
117 | td.standings-username:hover {
118 | cursor: pointer;
119 | text-decoration: underline;
120 | }
121 |
122 | .table-sort th{
123 | background-image: none !important;
124 | }
125 |
126 | .pagination .me a {
127 | background-color: rgba(252, 0, 0, 0.09);
128 | color : rgb(114,0,0);
129 | }
130 |
131 | .pagination .active-me a {
132 | background-color: #f5f5f5;
133 | color : rgb(200,0,0);
134 | }
135 | `;
136 |
137 | $('head').append(``);
138 | }
--------------------------------------------------------------------------------
/src/filter.js:
--------------------------------------------------------------------------------
1 | import {countries, rating} from './util.js';
2 | import AppSettings from './appSettings.js';
3 |
4 | class FilterContent extends React.Component {
5 | constructor(props){
6 | super(props);
7 |
8 | this.byFriendsList.bind(this);
9 | this.byCountry.bind(this);
10 | this.byRating.bind(this);
11 | this.byName.bind(this);
12 | this.update.bind(this);
13 | }
14 |
15 | update( option ){
16 | let newSettings = Object.assign(new AppSettings(), this.props.settings);
17 | for(let param in option){
18 | newSettings[param] = option[param];
19 | }
20 | this.props.settingsUpdateFunc( newSettings );
21 | }
22 |
23 | byFriendsList(){
24 | return (
25 | this.update( {"filterByFriends": !this.props.settings.filterByFriends} )}>
28 |
29 | Friends
30 |
31 |
32 | );
33 | }
34 |
35 | byCountry(){
36 | const form = this.props.getActiveCountries().map( (country) => {
37 | const val = countries[country];
38 | return ({val} );
39 | });
40 | return (
41 |
42 |
this.update( {"filterByCountry": !this.props.settings.filterByCountry} )}
45 | >
46 | Country
47 |
48 |
49 | {this.update( {"filterByCountry":true, "filterCountry": e.target.value} )} }>
51 | {form}
52 |
53 |
54 |
55 | );
56 | }
57 |
58 | byRating(){
59 | let buttons = rating.lb.map( (lb, idx) => {
60 | if(idx === 0) return "";
61 | if( this.props.settings.filterRating.has(idx) === true ){
62 | return (
63 | {
64 | let obj = new Set( this.props.settings.filterRating );
65 | obj.delete( idx );
66 | this.update( {"filterByRating":true, "filterRating": obj} );
67 | }} title={`${lb} - `} key={`rating-filter-rating-${lb}`}>
68 | check_box
69 |
70 | );
71 | }else{
72 | return (
73 | {
74 | let obj = new Set( this.props.settings.filterRating );
75 | obj.add( idx );
76 | this.update( {"filterByRating":true, "filterRating": obj} );
77 | }} title={`${lb} - `} key={`rating-filter-rating-${lb}`}>
78 | check_box_outline_blank
79 |
80 | );
81 | }
82 | });
83 |
84 | let tool = (()=>{
85 | return(
86 |
87 | {
88 | let obj = new Set([1,2,3,4]);
89 | this.update( {"filterByRating":true, "filterRating": obj} );
90 | }} title="0-1199">{"ABC"}
91 |
92 | {
93 | let obj = new Set([1,2,3,4,5,6,7,8]);
94 | this.update( {"filterByRating":true, "filterRating": obj} );
95 | }} title="0-2799">{"ARC"}
96 |
97 | {
98 | let obj = new Set();
99 | this.update( {"filterByRating":true, "filterRating": obj} );
100 | }}>{"None"}
101 |
102 | {
103 | let obj = new Set([1,2,3,4,5,6,7,8,9]);
104 | this.update( {"filterByRating":true, "filterRating": obj} );
105 | }}>{"All"}
106 |
107 | );
108 | })();
109 |
110 | return (
111 |
112 |
this.update( {"filterByRating": !this.props.settings.filterByRating} )}
115 | >
116 | Rating
117 |
118 |
119 |
{buttons}
120 |
{tool}
121 |
122 |
123 | );
124 | }
125 |
126 | byName(){
127 | return (
128 |
129 |
this.update( {"filterByName": !this.props.settings.filterByName} )}
132 | >
133 | Name
134 |
135 |
136 | {
137 | this.update( {"filterName": e.target.value, "filterByName": true} )
138 | } } />
139 |
140 |
141 | );
142 | }
143 |
144 | render(){
145 | const byFriend = this.byFriendsList();
146 | const byRating = this.byCountry();
147 | const byCountry = this.byRating();
148 | const byName = this.byName();
149 | return (
150 | e.stopPropagation()}>
160 |
161 | {byFriend}
162 | {byRating}
163 | {byCountry}
164 | {byName}
165 |
166 |
167 | );
168 | }
169 |
170 | }
171 |
172 | export default class Filter extends React.Component {
173 | constructor(props){
174 | super(props);
175 |
176 | this.state = {
177 | show : false,
178 | posX : 0,
179 | posY : 0
180 | };
181 | }
182 |
183 | render(){
184 | const button = (
185 |
187 | filter_list
188 | Filter
189 |
190 | );
191 |
192 | if( this.state.show === false ){
193 | return (
194 |
195 |
{
196 | let rect = e.target.getBoundingClientRect();
197 | this.setState( {show : !this.state.show, posX:rect.left, posY:rect.top }) ;
198 | } }>{button}
199 |
200 | );
201 | }else{
202 | return(
203 |
204 |
this.setState( {show : !this.state.show }) }>{button}
205 |
this.setState({show:false})}>
207 |
212 |
213 |
214 | );
215 | }
216 | }
217 | }
--------------------------------------------------------------------------------
/src/friendsList.js:
--------------------------------------------------------------------------------
1 | export default class FriendsList{
2 | constructor( load ){
3 | this.friends = new Set();
4 | if(load === true) this.load();
5 |
6 | //this.add("camypaper");
7 | }
8 |
9 | load(){
10 | //load
11 | //friend list object (old version)
12 | let friendsOld = JSON.parse( GM_getValue('GM_friend_list', 'null') );
13 | if(friendsOld !== null){
14 | this.friends = new Set( Object.keys(friendsOld) );
15 | GM_deleteValue( 'GM_friend_list' );
16 | this.save();
17 | }
18 |
19 | //friend list array (new version)
20 | this.friends = new Set(JSON.parse( GM_getValue('friendsList', '[]') ));
21 |
22 | console.log("loaded : friends list");
23 | console.log(this.friends);
24 | }
25 |
26 | save(){
27 | let str = JSON.stringify([...this.friends]);
28 | //save
29 | GM_setValue('friendsList', str);
30 |
31 | console.log("saved : friends list");
32 | console.log(str);
33 | }
34 |
35 | //[names...]
36 | add(handle){
37 | handle.forEach( (name) => this.friends.add(name) );
38 | this.save();
39 | }
40 |
41 | remove(handle){
42 | handle.forEach( (name) => this.friends.delete(name) );
43 | this.save();
44 | }
45 |
46 |
47 | isFriend(handle){
48 | return this.friends.has( handle );
49 | }
50 |
51 | getList(){
52 | return [...this.friends];
53 | }
54 | }
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | //import React from 'react';
2 | //import ReactDOM from 'react-dom';
3 | import AtCoderCutomStandings from './app.js'
4 | import injectCustomCSS from './css.js'
5 |
6 | $('div.table-responsive').hide();
7 | $('#pagination-standings').hide();
8 | $('#standings-csv-link').after('
');
9 | //$('head').append(' ');
10 | injectCustomCSS();
11 |
12 | try{
13 | ReactDOM.render(
14 | ,
15 | document.getElementById('content')
16 | );
17 | }catch(e){
18 | console.log( "some error occurred" );
19 | console.log( e );
20 | $('div.table-responsive').show();
21 | $('#pagination-standings').show();
22 | }
23 |
--------------------------------------------------------------------------------
/src/modal.js:
--------------------------------------------------------------------------------
1 | class ModalWindow extends React.Component{
2 | constructor(props){
3 | super(props);
4 | }
5 | render(){
6 | let head = (
7 |
8 |
{this.props.title}
9 |
clear
10 |
11 | );
12 |
13 | return (
14 |
15 | {head}
16 | {this.props.children}
17 |
18 | );
19 | }
20 | }
21 |
22 | export default class Modal extends React.Component{
23 |
24 | constructor(props){
25 | super(props);
26 | this.state = {show: false};
27 | }
28 |
29 | render(){
30 | let button = (
31 | {this.setState( {show: true} ); } }
32 | className="atcoder-custom-standings controller-button">
33 | {this.props.button}
34 |
35 | );
36 |
37 | if( this.state.show === true ){
38 | return(
39 |
40 | {button}
41 |
{ this.setState({ show: false}) } }>
42 |
{e.stopPropagation(); return false;} }>
43 | { this.setState({ show: false}) } } title={this.props.title}>
44 | {this.props.children}
45 |
46 |
47 |
48 |
49 | );
50 | }else{
51 | return(
52 |
53 | {button}
54 |
55 | );
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/pager.js:
--------------------------------------------------------------------------------
1 | class PageButton extends React.Component{
2 | constructor(props){
3 | super();
4 | }
5 |
6 | shouldComponentUpdate( nextProps ){
7 | if( this.props.current !== nextProps.current ) return true;
8 | if( this.props.me !== nextProps.me ) return true;
9 | return false;
10 | }
11 |
12 | render(){
13 | const p = this.props.page;
14 |
15 | if( this.props.current === p ){
16 | return ({p + 1} );
17 | }else{
18 | return ({p + 1} );
19 | }
20 | }
21 | }
22 |
23 | export default class Pager extends React.Component {
24 | /**
25 | * @param {number} current current page (0-indexed)
26 | * @param {number} total total page
27 | * @param {number} me page where i am
28 | * @param {function} onClickFunc
29 | */
30 | constructor(props){
31 | super(props);
32 | }
33 |
34 | shouldComponentUpdate( nextProps ){
35 | if( this.props.current !== nextProps.current ) return true;
36 | if( this.props.total !== nextProps.total ) return true;
37 | if( this.props.me !== nextProps.me ) return true;
38 | return false;
39 | }
40 |
41 | render(){
42 | let showingPages = new Array();
43 | for(let page=0; page 0 && showingPages[i] - showingPages[i-1] > 1){
53 | if( showingPages[i] - showingPages[i-1] === 2 ){
54 | res.push( );
59 | }else{
60 | res.push( {"..."} );
61 | }
62 | }
63 | res.push( );
68 | }
69 |
70 | return ();
71 | }
72 | }
--------------------------------------------------------------------------------
/src/reload.js:
--------------------------------------------------------------------------------
1 | export default class Reloading extends React.Component {
2 | constructor(props){
3 | super(props);
4 | this.state = { autoUpdate:false };
5 | }
6 |
7 | render(){
8 | return (
9 |
15 |
{
17 | if(!this.state.autoUpdate){
18 | this.timerReloading = setInterval( this.props.updateFunc, 60*1000 );
19 | console.log( "create timer ", this.timerReloading);
20 | }else{
21 | try{
22 | clearInterval( this.timerReloading );
23 | console.log( "erase timer ", this.timerReloading);
24 | }catch(e){
25 |
26 | }
27 | }
28 | this.setState( {autoUpdate:!this.state.autoUpdate})
29 | } }>
30 |
31 | update Auto (1min)
32 |
33 |
34 |
);
35 | }
36 | }
--------------------------------------------------------------------------------
/src/settings.js:
--------------------------------------------------------------------------------
1 | import Modal from './modal.js'
2 | import AppSettings from './appSettings.js'
3 | class SettingsContent extends React.Component{
4 | constructor(props){
5 | super(props);
6 | this.update.bind(this);
7 | this.generateForm.bind(this);
8 | this.generateFriendsListForm.bind(this);
9 | }
10 |
11 | update( option ){
12 | let newSettings = Object.assign(new AppSettings(), this.props.settings);
13 | for(let param in option){
14 | newSettings[param] = option[param];
15 | }
16 | console.log( option );
17 | this.props.settingsUpdateFunc( newSettings );
18 | }
19 |
20 | generateForm( optionName, label ){
21 | return (
22 |
23 |
24 | { this.update( { [optionName] : e.target.checked } ) }} />
26 | {label}
27 |
28 |
29 | );
30 | }
31 |
32 | generateFriendsListForm(){
33 | const friends = this.props.friends.getList().map( (name) => {
34 | return ({name} );
35 | });
36 | return(
37 |
38 |
Friends List
39 |
40 | {
42 | if( e.key !== 'Enter' ) return;
43 | const element = this.refs.addFriendForm;
44 | if( element.value !== "" ) this.props.friendsUpdateFunc( element.value.split(" "), true );
45 | element.value = "";
46 | this.forceUpdate();
47 | }}/>
48 | {
49 | const element = this.refs.addFriendForm;
50 | if( element.value !== "" ) this.props.friendsUpdateFunc( [element.value], true );
51 | element.value = "";
52 | this.forceUpdate();
53 | }}>
54 | Add Friend
55 |
56 |
57 |
58 |
59 | {friends}
60 |
61 | {
62 | const form = this.refs.friendsListForm;
63 | this.props.friendsUpdateFunc( [...form.getElementsByTagName('option')]
64 | .filter( (e)=>e.selected ).map((e)=>e.value), false );
65 | this.forceUpdate();
66 | }}>
67 | Remove Friends
68 |
69 |
70 |
71 | );
72 | }
73 |
74 | render(){
75 | const pageSize = (()=>{
76 | const list = [10,20,50,100,200,300,400,500,1000,5000,10000].map( (val)=>{
77 | return {val}
78 | });
79 | return (
80 |
81 | Page Size
82 | { this.update( { "pageSize" : Number(e.target.value)} ) }}>
84 | {list}
85 |
86 |
87 | );
88 | })();
89 |
90 | const displayNameStyle = (
91 |
92 | Display Name Style
93 | { this.update( { "displayNameStyle" : e.target.value} ) }}>
95 | User ID
96 | User Name
97 | User ID / User Name
98 | User Name / User ID
99 |
100 |
101 | );
102 |
103 |
104 | return (
105 |
106 | {pageSize}
107 | {displayNameStyle}
108 | {this.generateForm( "disableRatingColor", "Disable Rating Color")}
109 | {this.generateForm( "highlightFriends", "Highlight Friends")}
110 | {this.generateForm( "showNationalFlag", "Show National Flag")}
111 | {this.generateForm( "saveFilteringState", "Save Filtering State (if this option is checked, your filtering state will be restored when you open standings page)")}
112 |
113 | {this.generateFriendsListForm()}
114 |
115 | );
116 | }
117 | }
118 |
119 | export default class Settings extends React.Component {
120 | constructor(props){
121 | super(props);
122 | }
123 |
124 | shouldComponentUpdate( nextProps ){
125 | if( JSON.stringify( Object.assign({}, this.props.settings) ) !== JSON.stringify( Object.assign({}, nextProps.settings) )) return true;
126 | // if( JSON.stringify( this.props.friends.getList() ) !== JSON.stringify( nextProps.friends.getList() ) ) return true;
127 | return true;
128 | }
129 |
130 | render(){
131 | let button = (
132 |
133 | settings
134 | Settings
135 |
136 | );
137 |
138 | return(
139 |
140 |
146 |
147 | );
148 | }
149 | }
--------------------------------------------------------------------------------
/src/sorting.js:
--------------------------------------------------------------------------------
1 | import AppSettings from './appSettings.js';
2 |
3 | class SortingContent extends React.Component{
4 | constructor(props){
5 | super(props);
6 | this.update.bind(this);
7 | }
8 |
9 | update( option ){
10 | let newSettings = Object.assign(new AppSettings(), this.props.settings);
11 | for(let param in option){
12 | newSettings[param] = option[param];
13 | }
14 | this.props.settingsUpdateFunc( newSettings );
15 | }
16 |
17 | render(){
18 | let onOff = ;
23 |
24 | let keys = [];
25 | keys.push( this.update( {
27 | sortingKey : "rank",
28 | sortingEnabled:true,
29 | sortingOrder: this.props.settings.sortingKey !== "rank" ? "ascending" : this.props.settings.sortingOrder === "ascending" ? "descending" : "ascending" ,
30 | } )} key="rank">Rank );
31 |
32 | keys.push( this.update( {
34 | sortingKey : "time",
35 | sortingEnabled:true,
36 | sortingOrder: this.props.settings.sortingKey !== "time" ? "ascending" : this.props.settings.sortingOrder === "ascending" ? "descending" : "ascending" ,
37 | } )} key="time">Time(without penalty) );
38 |
39 | keys.push( this.update( {
41 | sortingKey : "user_screen_name",
42 | sortingEnabled:true,
43 | sortingOrder: this.props.settings.sortingKey !== "user_screen_name" ? "ascending" : this.props.settings.sortingOrder === "ascending" ? "descending" : "ascending" ,
44 | } )} key="user_screen_name">Name );
45 |
46 | keys.push( this.update( {
48 | sortingKey : "rating",
49 | sortingEnabled:true,
50 | sortingOrder: this.props.settings.sortingKey !== "rating" ? "descending" : this.props.settings.sortingOrder === "ascending" ? "descending" : "ascending" ,
51 | } )} key="rating">Rating );
52 | keys.push( this.update( {
54 | sortingKey : "country",
55 | sortingEnabled:true,
56 | sortingOrder: this.props.settings.sortingKey !== "country" ? "ascending" : this.props.settings.sortingOrder === "ascending" ? "descending" : "ascending" ,
57 | } )} key="country">Country );
58 |
59 | let keysTasks = [];
60 | for(let i=0; i this.update( {
63 | sortingKey : `task${i}`,
64 | sortingEnabled:true,
65 | sortingOrder: this.props.settings.sortingKey !== `task${i}` ? "descending" : this.props.settings.sortingOrder === "ascending" ? "descending" : "ascending" ,
66 | } )} key={`task${i}`}>Task-{"ABCDEFGHIJKLMNOPQRSTUVWXYZ"[i]} );
67 | }
68 |
69 | let order;
70 | if( this.props.settings.sortingOrder === "ascending"){
71 | order = this.update( {sortingOrder: "descending", sortingEnabled:true} )}>
72 | sort Ascending
73 | ;
74 | }else{
75 | order = this.update( {sortingOrder: "ascending", sortingEnabled:true} )}>
76 | sort Descending
77 | ;
78 | }
79 | return (
80 | e.stopPropagation()} >
88 |
89 |
{onOff}
90 |
Key :
91 |
{keys} {keysTasks}
92 |
Order :
93 |
{order}
94 |
95 |
96 | );
97 | }
98 | }
99 |
100 | export default class Sorting extends React.Component{
101 | constructor(props){
102 | super(props);
103 | this.state = {
104 | show : false,
105 | posX : 0,
106 | posY : 0
107 | };
108 |
109 | }
110 | render(){
111 | let button = (
112 |
113 | sort
114 | Sort
115 |
116 | );
117 |
118 | if( this.state.show === false ){
119 | return (
120 |
121 |
{
122 | let rect = e.target.getBoundingClientRect();
123 | this.setState( {show : !this.state.show, posX:rect.left, posY:rect.top }) ;
124 | } }>
125 | {button}
126 |
127 |
128 | );
129 | }else{
130 | return (
131 |
132 |
this.setState({show:!this.state.show})} >{button}
133 |
this.setState({show:false})}>
135 |
140 |
141 |
142 | );
143 | }
144 | }
145 | }
--------------------------------------------------------------------------------
/src/standings.js:
--------------------------------------------------------------------------------
1 | import {rating,countries} from './util.js'
2 | import Me from './userinfo.js';
3 |
4 | class UserDetails extends React.Component{
5 | constructor(props){
6 | super(props);
7 | }
8 |
9 | componentDidMount(){
10 | document.getElementById(`user-dropdown-menu-${this.props.row.user_name}`).addEventListener('click', (e)=>{
11 | e.stopPropagation();
12 | });
13 | document.getElementById(`user-dropdown-menu-${this.props.row.user_name}-friend`).addEventListener('click', (e)=>{
14 | this.props.friendsUpdateFunc( [this.props.row.user_screen_name], !this.props.isFriend);
15 | });
16 | document.getElementsByTagName('body')[0].addEventListener('click', this.props.closeFunc, {once:true});
17 | }
18 |
19 | render(){
20 | const link = `https://atcoder.jp/user/${this.props.row.user_screen_name}`;
21 | const submissions = (
22 |
23 | search
24 | Submissions
25 |
26 | );
27 | const ratingColor = rating.getColorOriginal(this.props.row.rating);
28 |
29 | const friend = (
30 |
31 | {this.props.isFriend ? "person_outline" : "person_add"}
32 | {this.props.isFriend ? "Remove from Friends List" : "Add to Friends List"}
33 |
34 | );
35 | const twitter = this.props.row.twitter_id == "" ? "" : (
36 | );
42 |
43 | return (
44 |
71 | );
72 | }
73 | }
74 |
75 |
76 | class Name extends React.Component{
77 | constructor(props){
78 | super(props);
79 | this.state = {
80 | show : false
81 | };
82 | }
83 |
84 | shouldComponentUpdate( nextProps, nextState ){
85 | if( this.props.settings.displayNameStyle !== nextProps.settings.displayNameStyle ) return true;
86 | if( this.props.settings.disableRatingColor !== nextProps.settings.disableRatingColor ) return true;
87 | if( this.props.settings.showNationalFlag !== nextProps.settings.showNationalFlag ) return true;
88 | if( this.state.show !== nextState.show ) return true;
89 | if( this.props.isFriend !== nextProps.isFriend ) return true;
90 | return false;
91 | }
92 |
93 | render(){
94 | const row = this.props.row;
95 | const color = this.props.settings.disableRatingColor ? "" : rating.userColor[ rating.getLevel(row.rating) ];
96 |
97 | const displayName = (()=>{
98 | return {
99 | user_screen_name : row.user_screen_name,
100 | user_name : row.user_name,
101 | user_screen_name_user_name : `${row.user_screen_name} / ${row.user_name}`,
102 | user_name_user_screen_name : `${row.user_name} / ${row.user_screen_name}`
103 | }[this.props.settings.displayNameStyle];
104 | })();
105 |
106 | const countryFlag = this.props.settings.showNationalFlag ? ( ) : "";
107 |
108 | const nameOnclick = (e) => {
109 | this.setState({
110 | show : !this.state.show
111 | });
112 | };
113 |
114 | if( this.state.show === false ){
115 | return (
116 |
117 | {countryFlag}
118 | {" "}
119 | {row.rating >= 3200 ? : null}
120 | {row.rating >= 3200 ? " " : null}
121 | {displayName}
122 |
123 | );
124 | }else{
125 | return (
126 | this.setState({show:false})}>
127 | {countryFlag}
128 | {" "}
129 | {row.rating >= 3200 ? : null}
130 | {row.rating >= 3200 ? " " : null}
131 | {displayName}
132 | this.setState({show:false})}/>
137 |
138 | );
139 | }
140 | }
141 | }
142 |
143 | class Task extends React.Component{
144 | constructor(props){
145 | super(props);
146 | }
147 |
148 | shouldComponentUpdate( nextProps ){
149 | if( JSON.stringify(this.props.task) !== JSON.stringify(nextProps.task) ) return true;
150 | return false;
151 | }
152 |
153 | render(){
154 | const t = this.props.task;
155 | if( t.extras === true && this.props.me === false ){
156 | return
157 | }
158 | if( t.elapsed_time === undefined ){
159 | return -
160 | }
161 | if( t.score === 0 ){
162 | return ({t.failure})
163 | }
164 | let penalty = "";
165 | if(t.failure !== 0){
166 | penalty = ({t.failure}) ;
167 | }
168 |
169 | let submission = this.props.contestEnded ?
170 | search
171 | : "";
172 |
173 | const timeMin = `${Math.floor(t.elapsed_time/60)<10?"0":""}${Math.floor(t.elapsed_time/60)}`;
174 | const timeSec = `00${Math.floor(t.elapsed_time%60)}`.slice(-2);
175 | return (
176 |
177 | {t.score/100} {penalty}{submission}
178 | {timeMin}:{timeSec}
179 |
180 | );
181 | }
182 | }
183 |
184 | class Total extends React.Component{
185 | constructor(props){
186 | super(props);
187 | }
188 |
189 | shouldComponentUpdate( nextProps ){
190 | const comp = ["elapsed_time", "failure", "penalty", "score"];
191 | for(const param of comp){
192 | if( this.props.row[param] !== nextProps.row[param] ) return true;
193 | }
194 | return false;
195 | }
196 |
197 | render(){
198 | if( this.props.row.elapsed_time === "0" ){
199 | return -
;
200 | }
201 | let penalty = "";
202 | if(this.props.row.failure !== "0"){
203 | penalty = ({this.props.row.failure}) ;
204 | }
205 | const timeMin = `${Math.floor(this.props.row.elapsed_time/60)<10?"0":""}${Math.floor(this.props.row.elapsed_time/60)}`;
206 | const timeSec = `00${Math.floor(this.props.row.elapsed_time%60)}`.slice(-2);
207 |
208 | const penaltyMin = `${Math.floor(this.props.row.penalty/60)<10?"0":""}${Math.floor(this.props.row.penalty/60)}`;
209 | const penaltySec = `00${Math.floor(this.props.row.penalty%60)}`.slice(-2);
210 | return (
211 |
212 | {this.props.row.score/100} {penalty}
213 | {penaltyMin}:{penaltySec} ({timeMin}:{timeSec})
214 |
215 | );
216 | }
217 | }
218 |
219 | /*
220 | rank
221 | name
222 | id
223 | rating
224 | country
225 | tasks[]
226 | score
227 | elapsed_time
228 | penalty
229 | */
230 | class StandingsRow extends React.Component {
231 | constructor(props){
232 | super(props);
233 | }
234 |
235 | shouldComponentUpdate( nextProps ){
236 | if( JSON.stringify( Object.assign({}, this.props.settings) ) !== JSON.stringify( Object.assign({}, nextProps.settings) ) ) return true;
237 | if( JSON.stringify(this.props.row) !== JSON.stringify(nextProps.row) ) return true;
238 | if( this.props.isFriend !== nextProps.isFriend ) return true;
239 | if( this.props.filteredRank !== nextProps.filteredRank ) return true;
240 | if( this.props.contestEnded !== nextProps.contestEnded ) return true;
241 | return false;
242 | }
243 |
244 | render(){
245 | const name = ;
249 |
250 | const tasks = this.props.row.tasks.map( (t, i) => {
251 | return ;
256 | });
257 |
258 | const total = ;
259 |
260 | let trClass = "";
261 | if( this.props.isFriend && this.props.settings.highlightFriends === true ) trClass = "standings-friend";
262 | if( Me.contestant === true && this.props.row.user_id === Me.user_id ) trClass = "standings-me";
263 |
264 | return (
265 |
266 |
267 | {this.props.row.rank}{this.props.settings.isFiltersEnabled() || this.props.settings.sortingEnabled ?` (${this.props.filteredRank})`:""}
268 |
269 | {name}
270 | {tasks}
271 | {total}
272 |
273 | );
274 | }
275 | }
276 |
277 | class StandingsHead extends React.Component {
278 | constructor(props){
279 | super(props);
280 | }
281 | shouldComponentUpdate(){
282 | return false;
283 | }
284 | render(){
285 | const tasks = this.props.taskData.map( (t, i) => {
286 | return (
287 | {t.name}
288 | );
289 | });
290 | return (
291 |
292 | {"Rank"}
293 | {"User Name"}
294 | {tasks}
295 | {"Score / Time"}
296 |
297 | );
298 | }
299 | }
300 |
301 | export default class Standings extends React.Component {
302 | constructor(props){
303 | super(props);
304 | }
305 |
306 | render(){
307 | let standingsRows = "";
308 | if( this.props.standings.length > 0 ){
309 | standingsRows = this.props.standings.map( (row, i) => {
310 | let isFriend = this.props.friends.isFriend( row.user_screen_name );
311 | return
319 | } );
320 | }
321 |
322 | return (
323 |
324 |
325 |
326 |
327 |
328 | {standingsRows}
329 |
330 |
331 | );
332 | }
333 | }
--------------------------------------------------------------------------------
/src/stats.js:
--------------------------------------------------------------------------------
1 | import StatsSummary from './stats/summary.js'
2 | import StatsTask from './stats/task.js'
3 | import Modal from './modal.js'
4 |
5 | class StatsContent extends React.Component{
6 | constructor(props){
7 | super(props);
8 | this.state = { page: 0 };
9 | }
10 |
11 | shouldComponentUpdate( nextProps, nextState ){
12 | return this.state.page !== nextState.page;
13 | }
14 |
15 | render(){
16 | let tab = this.props.contest.tasks.map( (t,i) => {
17 | if( this.state.page === i ){
18 | return ({'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[i]} );
19 | }else{
20 | return ({
21 | this.setState({page:i});
22 | }}>{'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[i]} );
23 | }
24 | });
25 |
26 | let component;
27 | if(this.state.page === this.props.contest.numTasks){
28 | tab.push( Summary );
29 | component = (
30 |
32 | );
33 | }else{
34 | tab.push( {
35 | this.setState({page:this.props.contest.numTasks});
36 | } }>Summary );
37 | component = (
38 |
41 | );
42 | }
43 |
44 | return (
45 |
46 |
49 | {component}
50 |
51 | );
52 | }
53 | }
54 |
55 | export default class Stats extends React.Component {
56 | /**
57 | * @param props.standings
58 | * @param props.contest
59 | */
60 | constructor(props){
61 | super(props);
62 | }
63 |
64 | render(){
65 | let button = (
66 | assessment Statistics
67 | );
68 |
69 | return (
70 |
71 |
72 |
73 | );
74 | }
75 | }
--------------------------------------------------------------------------------
/src/stats/chartComponent.js:
--------------------------------------------------------------------------------
1 | export default class ChartComponent extends React.Component{
2 | /**
3 | * canvasId
4 | * dataset
5 | * width
6 | * height
7 | */
8 | constructor(props){
9 | super(props);
10 | }
11 | render(){
12 | return(
13 |
14 |
15 |
16 | );
17 | }
18 | componentDidMount(){
19 | let ctx = document.getElementById(this.props.canvasId);
20 | // console.log(ctx);
21 | this.chart = new Chart(ctx, this.props.dataset);
22 | // console.log(this.chart);
23 | }
24 | componentWillUnmount(){
25 | this.chart.destroy();
26 | }
27 | componentDidUpdate(){
28 | this.chart.destroy();
29 | let ctx = document.getElementById(this.props.canvasId);
30 | // console.log(ctx);
31 | this.chart = new Chart(ctx, this.props.dataset);
32 | // console.log(this.chart);
33 | }
34 | }
--------------------------------------------------------------------------------
/src/stats/summary.js:
--------------------------------------------------------------------------------
1 | import {rating,countries} from '../util.js'
2 | import ChartComponent from './chartComponent.js'
3 |
4 | class TopOfColors extends React.Component{
5 | constructor(props){
6 | super(props);
7 | }
8 | render(){
9 | let data = new Array(rating.lb.length);
10 | data.fill(undefined);
11 |
12 | this.props.standings.forEach( (s)=>{
13 | if(s.elapsed_time === "0") {
14 | let participating = false;
15 | s.tasks.forEach( (t)=>{
16 | if(t.score !== undefined) participating = true;
17 | } );
18 | if( participating === false ) return;
19 | }
20 |
21 | const level = rating.getLevel( s.rating );
22 | if( data[level] === undefined ){
23 | data[level] = {
24 | name : s.user_screen_name,
25 | rating : s.rating,
26 | rank : s.rank,
27 | score : Number(s.score) / 100,
28 | time : Number(s.elapsed_time),
29 | penalty : Number(s.penalty),
30 | failure : Number(s.failure)
31 | };
32 | }
33 | });
34 |
35 | // console.log(data);
36 |
37 | data = data.slice(1);
38 |
39 | let comp = data.map( (d, idx) => {
40 | if( d === undefined ){
41 | return (
42 |
43 | {rating.lb[idx+1]} -
44 | -
45 | -
46 | -
47 | -
48 |
49 | );
50 | }else{
51 | return(
52 |
53 | {rating.lb[idx+1]} -
54 | {rating.generateColoredName( d.name, d.rating )}
55 | {d.rank}
56 | {d.score}{d.failure!=0? ({d.failure}) : ""}
57 | {Math.floor(d.time/60)} min {d.time%60} sec ({Math.floor(d.penalty/60)} min {d.penalty%60} sec)
58 |
59 | );
60 | }
61 | } );
62 |
63 | comp.reverse();
64 |
65 | return (
66 |
67 |
Top of Colors
68 |
69 |
70 |
71 | Rating
72 | Top
73 | Rank
74 | Score (Penalty)
75 | Time (Penalty)
76 |
77 |
78 |
79 | {comp}
80 |
81 |
82 |
83 | );
84 | }
85 |
86 | }
87 |
88 | class TopOfCountries extends React.Component{
89 | constructor(props){
90 | super(props);
91 | this.state = {
92 | sortingKey : "rank",
93 | ascending : true
94 | };
95 | }
96 |
97 | render(){
98 | let data = {};
99 |
100 | this.props.standings.forEach( (s)=>{
101 | if(s.elapsed_time === "0") {
102 | let participating = false;
103 | s.tasks.forEach( (t)=>{
104 | if(t.score !== undefined) participating = true;
105 | } );
106 | if( participating === false ) return;
107 | }
108 |
109 | const country = s.country;
110 |
111 | if( data[country] === undefined ){
112 | data[country] = {
113 | name : s.user_screen_name,
114 | country : s.country,
115 | rating : s.rating,
116 | rank : s.rank,
117 | score : Number(s.score) / 100,
118 | time : Number(s.elapsed_time),
119 | penalty : Number(s.penalty),
120 | failure : Number(s.failure),
121 | scoreTime : Number(s.score) * 1000000000 - Number(s.penalty)
122 | };
123 | }
124 | });
125 |
126 | data = Object.keys(data).map( (c) => {
127 | return data[c];
128 | } );
129 |
130 | data.sort( (x,y) => {
131 | let res = x[this.state.sortingKey] < y[this.state.sortingKey];
132 | res = this.state.ascending?res:!res;
133 | return res ? -1 : 1;
134 | } );
135 |
136 | let comp = data.map( (d, idx) => {
137 | return(
138 |
139 | {d.rank}
140 | {countries[d.country]}
141 | {rating.generateColoredName( d.name, d.rating )}
142 | {d.score}{d.failure!=0? ({d.failure}) : ""}
143 | {Math.floor(d.time/60)} min {d.time%60} sec ({Math.floor(d.penalty/60)} min {d.penalty%60} sec)
144 |
145 | );
146 | } );
147 |
148 | return (
149 |
150 |
Top of Countries
151 |
152 |
153 |
154 | {
155 | if( this.state.sortingKey == "rank" ) this.setState ({ sortingKey : "rank", ascending : !this.state.ascending });
156 | else this.setState ({ sortingKey : "rank", ascending : true });
157 | }}>Rank
158 | {
159 | if( this.state.sortingKey == "country" ) this.setState ({ sortingKey : "country", ascending : !this.state.ascending });
160 | else this.setState ({ sortingKey : "country", ascending : true });
161 | }}>Country
162 | {
163 | if( this.state.sortingKey == "name" ) this.setState ({ sortingKey : "name", ascending : !this.state.ascending });
164 | else this.setState ({ sortingKey : "name", ascending : true });
165 | }}>Top
166 | {
167 | if( this.state.sortingKey == "scoreTime" ) this.setState ({ sortingKey : "scoreTime", ascending : !this.state.ascending });
168 | else this.setState ({ sortingKey : "scoreTime", ascending : false });
169 | }}>Score (Penalty)
170 | {
171 | if( this.state.sortingKey == "scoreTime" ) this.setState ({ sortingKey : "scoreTime", ascending : !this.state.ascending });
172 | else this.setState ({ sortingKey : "scoreTime", ascending : true });
173 | }}>Time (Penalty)
174 |
175 |
176 |
177 | {comp}
178 |
179 |
180 |
181 | );
182 | }
183 |
184 | }
185 |
186 | class NumberOfColorContestants extends React.Component{
187 | constructor(props){
188 | super(props);
189 | this.state = {
190 | sortingKey : "rating",
191 | ascending : false
192 | };
193 | }
194 |
195 | render(){
196 | let data = new Array( rating.lb.length );
197 | for(let i=0; i{
202 | if(s.elapsed_time === "0") {
203 | let participating = false;
204 | s.tasks.forEach( (t)=>{
205 | if(t.score !== undefined) participating = true;
206 | } );
207 | if( participating === false ) return;
208 | }
209 |
210 | const level = rating.getLevel( s.rating );
211 | data[level].contestants += 1;
212 |
213 | });
214 |
215 | data = data.slice(1);
216 |
217 | data.sort( (x,y) => {
218 | let res = x[this.state.sortingKey] < y[this.state.sortingKey];
219 | res = this.state.ascending?res:!res;
220 | return res ? -1 : 1;
221 | } );
222 |
223 | let comp = data.map( (d, idx) => {
224 | return(
225 |
226 | {rating.lb[d.rating]} -
227 | {d.contestants}
228 |
229 | );
230 | } );
231 |
232 | return (
233 |
234 |
Number of Contestants (Color)
235 |
236 |
237 |
238 | {
239 | if( this.state.sortingKey == "rating" ) this.setState ({ sortingKey : "rating", ascending : !this.state.ascending });
240 | else this.setState ({ sortingKey : "rating", ascending : false });
241 | }}>Rating
242 | {
243 | if( this.state.sortingKey == "contestants" ) this.setState ({ sortingKey : "contestants", ascending : !this.state.ascending });
244 | else this.setState ({ sortingKey : "contestants", ascending : false });
245 | }}>Contestants
246 |
247 |
248 |
249 | {comp}
250 |
251 |
252 |
253 | );
254 | }
255 |
256 | }
257 |
258 | class NumberOfCountryContestants extends React.Component{
259 | constructor(props){
260 | super(props);
261 | this.state = {
262 | sortingKey : "contestants",
263 | ascending : false
264 | };
265 | }
266 |
267 | render(){
268 | let data = {};
269 |
270 | this.props.standings.forEach( (s)=>{
271 | if(s.elapsed_time === "0") {
272 | let participating = false;
273 | s.tasks.forEach( (t)=>{
274 | if(t.score !== undefined) participating = true;
275 | } );
276 | if( participating === false ) return;
277 | }
278 |
279 | const country = s.country;
280 |
281 | if( data[country] === undefined ){
282 | data[country] = {
283 | country : country,
284 | contestants : 1
285 | };
286 | }else{
287 | data[country].contestants += 1;
288 | }
289 | });
290 |
291 | data = Object.keys(data).map( (c) => {
292 | return data[c];
293 | } );
294 |
295 | data.sort( (x,y) => {
296 | let res = x[this.state.sortingKey] < y[this.state.sortingKey];
297 | res = this.state.ascending?res:!res;
298 | return res ? -1 : 1;
299 | } );
300 |
301 | let comp = data.map( (d, idx) => {
302 | return(
303 |
304 | {countries[d.country]}
305 | {d.contestants}
306 |
307 | );
308 | } );
309 |
310 | return (
311 |
312 |
Number of Contestants (Country)
313 |
314 |
315 |
316 | {
317 | if( this.state.sortingKey == "country" ) this.setState ({ sortingKey : "country", ascending : !this.state.ascending });
318 | else this.setState ({ sortingKey : "country", ascending : true });
319 | }}>Country
320 | {
321 | if( this.state.sortingKey == "contestants" ) this.setState ({ sortingKey : "contestants", ascending : !this.state.ascending });
322 | else this.setState ({ sortingKey : "contestants", ascending : false });
323 | }}>Contestants
324 |
325 |
326 |
327 | {comp}
328 |
329 |
330 |
331 | );
332 | }
333 |
334 | }
335 |
336 |
337 |
338 |
339 | export default class StatsSummary extends React.Component{
340 | constructor(props){
341 | super(props);
342 | this.genDataset.bind(this);
343 | }
344 |
345 | genDataset(){
346 | const labels = rating.lb.slice(1).map( (r) => String(r) + " -" );
347 | const color = rating.color.slice(1);
348 | let count = rating.color.map( () => (new Map()) );
349 | let scoreDistribution = new Set();
350 | this.props.standings.forEach( (r) => {
351 | if( r.tasks.map( (t)=>t.elapsed_time !== undefined ? 1 : 0 ).reduce( (a,b)=>a+b ) !== 0 ){
352 | const level = rating.getLevel( r.rating );
353 | const score = r.score/100;
354 | scoreDistribution.add(score);
355 | count[level].set( score, count[level].has(score) ? count[level].get(score) + 1 : 1 );
356 | }
357 | });
358 | let scores = [...scoreDistribution].sort( (a,b) => { return a (new Array(scores.length)).fill(0) );
360 | count.forEach( (c, level) => {
361 | c.forEach( (cnt, score ) => {
362 | data[level][ scores.indexOf(score) ] = cnt;
363 | });
364 | });
365 |
366 | const dataset = {
367 | type : 'bar',
368 | data: {
369 | labels: scores,
370 | datasets: data.slice(1).map( (d, i) => {
371 | return {
372 | label: labels[i],
373 | data: d,
374 | backgroundColor: color[i]
375 | };
376 | })
377 | },
378 | options: {
379 | maintainAspectRatio : false,
380 | scales: {
381 | xAxes: [{
382 | display:true,
383 | scaleLabel:{
384 | display:true,
385 | labelString: "Score"
386 | },
387 | ticks: {
388 | beginAtZero:true
389 | }
390 | }],
391 | yAxes: [{
392 | display:true,
393 | scaleLabel:{
394 | display:true,
395 | labelString: "People"
396 | },
397 | ticks: {
398 | beginAtZero:true
399 | },
400 | stacked: true
401 | }]
402 | },
403 | animation : {
404 | animate: false,
405 | animateScale : false
406 | }
407 | }
408 | };
409 | return dataset;
410 | }
411 |
412 | render(){
413 |
414 | return (
415 |
416 |
417 | {this.props.contest.contestEnded ? This stats is unofficial. You can check the official stats here . : null}
418 |
419 |
420 |
Score Distribution
421 |
422 |
423 |
424 |
425 |
426 |
427 |
428 | );
429 | }
430 | }
--------------------------------------------------------------------------------
/src/stats/task.js:
--------------------------------------------------------------------------------
1 | import {rating} from '../util.js'
2 | import ChartComponent from './chartComponent.js'
3 |
4 | export default class StatsTask extends React.Component{
5 | /**
6 | * task
7 | * standings
8 | */
9 | constructor(props){
10 | super(props);
11 | this.timeStep = 5 * 60;
12 |
13 |
14 | this.getMaxScore.bind(this);
15 | this.getStatsValues.bind(this);
16 | this.generateDataset.bind(this);
17 | }
18 |
19 | getMaxScore(){
20 | let maxScore = 0;
21 | this.props.standings.forEach( (data) => {
22 | const d = data.tasks[ this.props.task.id ];
23 | if( d.score === undefined ) return;
24 | maxScore = Math.max(maxScore, Number(d.score));
25 | });
26 | return maxScore;
27 | }
28 |
29 | getStatsValues(standings){
30 | let res = {}
31 | try{
32 | res.numAC = 0;
33 | res.numWA = 0;
34 | res.numPeopleTried = 0;
35 | res.numSubmissions = 0;
36 | res.firstAcceptedTime = 0;
37 | res.firstAcceptedPerson = [];
38 |
39 | let timeSum = 0;
40 |
41 | res.numContestants = 0;
42 |
43 | //set FA
44 | standings.forEach( (data) => {
45 | const d = data.tasks[ this.props.task.id ];
46 | if( d.score === undefined ) return;
47 |
48 | if( this.maxScore == 0 || d.score != this.maxScore){
49 | return;
50 | }
51 |
52 | if( res.firstAcceptedTime == 0 ) res.firstAcceptedTime = Number(d.elapsed_time);
53 | else res.firstAcceptedTime = Math.min(res.firstAcceptedTime, Number(d.elapsed_time) );
54 | });
55 |
56 | //set other params
57 | standings.forEach( (data) => {
58 | //contestant made at least one submission
59 | if( data.tasks.map( (t)=>t.elapsed_time !== undefined ? 1 : 0 ).reduce( (a,b)=>a+b ) !== 0 ) res.numContestants++;
60 |
61 | const d = data.tasks[ this.props.task.id ];
62 | if( d.score === undefined ) return;
63 |
64 | res.numPeopleTried += 1;
65 | res.numSubmissions += d.failure;
66 | if( d.score != 0 ) res.numSubmissions += 1;
67 |
68 | if( this.maxScore == 0 || d.score != this.maxScore){
69 | return;
70 | }
71 |
72 | res.numAC += 1;
73 | res.numWA += d.failure;
74 | timeSum += d.elapsed_time;
75 |
76 | if( res.firstAcceptedTime == d.elapsed_time ){
77 | res.firstAcceptedPerson.push( rating.generateColoredName( data.user_screen_name, data.rating ) );
78 | res.firstAcceptedPerson.push( " " );
79 | }
80 |
81 | });
82 |
83 | if( res.numAC == 0 ){
84 | res.averageTime = 0;
85 | }else{
86 | res.averageTime = Math.round(timeSum / res.numAC);
87 | }
88 |
89 |
90 | }catch(e){
91 | console.log( "failed to generate stats" );
92 | console.log( e );
93 | }
94 |
95 | return res;
96 | }
97 |
98 | generateDataset(){
99 | const labels = rating.lb.slice(1).map( (r) => String(r) + "-" );
100 | const color = rating.color.slice(1);
101 | const contestDuration = (this.props.contest.endTime.getTime() - this.props.contest.startTime.getTime())/1000;
102 |
103 | // set solved histogram
104 | let data = rating.lb.map( () => (new Array( Math.floor( (contestDuration+this.timeStep-1) / this.timeStep ) )).fill(0) );
105 | this.props.standings.forEach( (r) => {
106 | const t = r.tasks[ this.props.task.id ];
107 | if( t.score !== 0 && t.score === this.maxScore ){
108 | data[ rating.getLevel( r.rating ) ][ Math.floor(t.elapsed_time / this.timeStep) ] += 1;
109 | }
110 | });
111 | // dataset for the chart
112 | const dataset = {
113 | type : 'bar',
114 | data: {
115 | labels : (()=>{
116 | let arr = new Array( Math.floor( (contestDuration+this.timeStep-1) / this.timeStep ) );
117 | for(let i=0; i {
123 | return {
124 | label: labels[i],
125 | data: d,
126 | backgroundColor: color[i]
127 | };
128 | })
129 | },
130 | options: {
131 | //responsive : false,
132 | maintainAspectRatio : false,
133 | scales: {
134 | xAxes: [{
135 | display:true,
136 | scaleLabel:{
137 | display:true,
138 | labelString: "Time [min]"
139 | },
140 | ticks: {
141 | beginAtZero:true
142 | }
143 | }],
144 | yAxes: [{
145 | display:true,
146 | scaleLabel:{
147 | display:true,
148 | labelString: "Solved"
149 | },
150 | ticks: {
151 | beginAtZero:true
152 | },
153 | stacked: true
154 | }]
155 | },
156 | animation : {
157 | animate: false,
158 | animateScale : false
159 | }
160 | }
161 | };
162 |
163 | return dataset;
164 | }
165 |
166 | render(){
167 | this.maxScore = this.getMaxScore();
168 | const dataAll = this.getStatsValues(this.props.standings);
169 | const rowAll = (
170 |
171 | ALL
172 | {dataAll.numAC}
173 | {dataAll.numPeopleTried}
174 | {dataAll.numSubmissions}
175 | {/*{( dataAll.numAC / Math.max(1, dataAll.numSubmissions) * 100).toFixed(2)}% */}
176 | {( dataAll.numAC / Math.max(1, dataAll.numPeopleTried) * 100).toFixed(2)}%
177 | {( dataAll.numAC / Math.max(1, dataAll.numContestants) * 100).toFixed(2)}%
178 | {dataAll.firstAcceptedPerson}
179 | {`${Math.floor( dataAll.firstAcceptedTime/60 )} min ${dataAll.firstAcceptedTime%60} sec`}
180 |
181 | {`${Math.floor( dataAll.averageTime/60 )} min ${dataAll.averageTime%60} sec`}
182 | {(dataAll.numWA / Math.max(1, dataAll.numAC)).toFixed(2)}
183 |
184 | );
185 |
186 | const dataColor = [];
187 | for(let r=1; r<=9; r++){
188 | const cStandings = this.props.standings.filter( (s)=>{
189 | return rating.lb[r] <= s.rating && s.rating < rating.ub[r];
190 | } );
191 | dataColor.push( this.getStatsValues(cStandings) );
192 | }
193 | const rowColor = dataColor.map( (data, idx) => {
194 | return (
195 |
196 | {rating.lb[idx+1]} -
197 | {data.numAC}
198 | {data.numPeopleTried}
199 | {data.numSubmissions}
200 | {/*{( data.numAC / Math.max(1, data.numSubmissions) * 100).toFixed(2)}% */}
201 | {( data.numAC / Math.max(1, data.numPeopleTried) * 100).toFixed(2)}%
202 | {( data.numAC / Math.max(1, data.numContestants) * 100).toFixed(2)}%
203 | {data.firstAcceptedPerson}
204 | {`${Math.floor( data.firstAcceptedTime/60 )} min ${data.firstAcceptedTime%60} sec`}
205 |
206 | {`${Math.floor( data.averageTime/60 )} min ${data.averageTime%60} sec`}
207 | {(data.numWA / Math.max(1, data.numAC)).toFixed(2)}
208 |
209 | );
210 | } ).reverse();
211 |
212 | try{
213 | const res = (
214 |
215 |
{'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[this.props.task.id]} : {this.props.task.name}
216 |
Max Score : {this.maxScore / 100}
217 |
218 |
219 |
220 | Rating
221 | AC
222 | Attempted
223 | Submissions
224 | {/*AC / Submissions */}
225 | AC / Attempted
226 | AC / Contestants
227 | Fastest
228 | Average Time
229 | Average WA
230 |
231 |
232 |
233 | {rowAll}
234 |
235 |
236 |
237 |
AC Time Distribution
238 |
240 |
241 |
242 |
Color Stats
243 |
244 |
245 |
246 | Rating
247 | AC
248 | Attempted
249 | Submissions
250 | {/*AC / Submissions */}
251 | AC / Attempted
252 | AC / Contestants
253 | Fastest
254 | Average Time
255 | Average WA
256 |
257 |
258 |
259 | {rowColor}
260 |
261 |
262 |
263 |
264 | );
265 | return res;
266 | }catch(e){
267 | console.log(e);
268 | }
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/src/userinfo.js:
--------------------------------------------------------------------------------
1 | class UserInfo{
2 | constructor(){
3 | let cookie = {};
4 | document.cookie.split(/;\s/).forEach( (s) => {
5 | //"_user_screen_name=koyumeishi; __privilege=contestant; _user_id=11408; _user_name=koyumeishi".split(/;\s/).forEach( (s) => {
6 | let [key, value] = s.split(/=/);
7 | cookie[key] = value;
8 | });
9 |
10 | this.contestant = false;
11 | if( "__privilege" in cookie && cookie.__privilege === "contestant"){
12 | this.contestant = true;
13 | this.user_screen_name = cookie._user_screen_name;
14 | this.user_id = Number( cookie._user_id );
15 | }
16 | console.log(this);
17 | }
18 | }
19 |
20 | const me = new UserInfo();
21 |
22 | export default me;
23 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | function getStandings( callback, initialize ){
2 | const reg = /\s*data:\s(\[.*\]),/;
3 |
4 | if(initialize){
5 | const scriptText = $("html").find('script[type="text/JavaScript"]').text().split("\n");
6 |
7 | scriptText.forEach( (txt) => {
8 | const res = reg.exec(txt);
9 | if(res !== null){
10 | const newStandings = JSON.parse(res[1]);
11 | callback( newStandings );
12 | }
13 | });
14 | }else{
15 | $.ajax( {url: "./standings"} ).done( (html) => {
16 | const scriptText = $(html).find('script[type="text/JavaScript"]').text().split("\n");
17 | scriptText.forEach( (txt) => {
18 | const res = reg.exec(txt);
19 | if(res !== null){
20 | console.log( "successfully got new standings : ", res[1] );
21 | const newStandings = JSON.parse(res[1]);
22 | callback( newStandings );
23 | }
24 | });
25 | });
26 | }
27 | }
28 |
29 | function getSortingFunction( key ){
30 | // task{i}
31 | if( key.slice(0,4) == "task" ){
32 | let id = Number( key.slice(4) );
33 | return (l,r) => {
34 | if( l.tasks[id].score === undefined && r.tasks[id].score === undefined ) return 0;
35 | if( l.tasks[id].score === undefined ) return -1;
36 | if( r.tasks[id].score === undefined ) return 1;
37 | if( l.tasks[id].score !== r.tasks[id].score ){
38 | return Number(l.tasks[id].score) < Number(r.tasks[id].score) ? -1 : 1;
39 | }else{
40 | if( l.tasks[id].penalty !== r.tasks[id].penalty ){
41 | return Number(l.tasks[id].penalty) > Number(r.tasks[id].penalty) ? -1 : 1;
42 | }else{
43 | return 0;
44 | }
45 | }
46 | };
47 | }
48 | if( key == "user_screen_name" ){
49 | return (l,r) =>{
50 | if( l[key].toLowerCase() !== r[key].toLowerCase() ){
51 | return l[key].toLowerCase() < r[key].toLowerCase() ? -1 : 1;
52 | }else{
53 | return 0;
54 | }
55 | };
56 | }
57 |
58 | if( key == "time" ){
59 | return (l,r) =>{
60 | if( l.score !== r.score ) return Number(l.score) > Number(r.score) ? -1 : 1;
61 | else if(l.elapsed_time !== r.elapsed_time) return Number(l.elapsed_time) < Number(r.elapsed_time) ? -1 : 1;
62 | return 0;
63 | };
64 | }
65 |
66 | return (l,r) => {
67 | if( l[key] !== r[key] ){
68 | return (l[key]) < (r[key]) ? -1 : 1;
69 | }else{
70 | return 0;
71 | }
72 | };
73 | }
74 |
75 | class Rating{
76 | constructor(){
77 | //[lb, ub)
78 | this.lb = [
79 | -1, 0, 1, 400, 800, 1200, 1600, 2000, 2400, 2800
80 | ];
81 | this.ub = [
82 | 0, 1, 400, 800, 1200, 1600, 2000, 2400, 2800, 5000
83 | ];
84 |
85 | this.color = [
86 | "rgba(192,0,192, 0.5)", // "#C000C0",
87 | "rgba(0,0,0, 0.5)", // "#000000",
88 | "rgba(128,128,128, 0.5)", // "#808080",
89 | "rgba(128,64,0, 0.5)", // "#804000",
90 | "rgba(0,128,0, 0.5)", // "#008000",
91 | "rgba(0,192,192, 0.5)", // "#00C0C0",
92 | "rgba(0,0,255, 0.5)", // "#0000FF",
93 | "rgba(192,192,0, 0.5)", // "#C0C000",
94 | "rgba(255,128,0, 0.5)", // "#FF8000",
95 | "rgba(255,0,0, 0.5)" // "#FF0000"
96 | ];
97 |
98 | this.colorOriginal = [
99 | "#C000C0",
100 | "#000000",
101 | "#808080",
102 | "#804000",
103 | "#008000",
104 | "#00C0C0",
105 | "#0000FF",
106 | "#C0C000",
107 | "#FF8000",
108 | "#FF0000"
109 | ];
110 |
111 | this.userColor = [
112 | "user-admin", // "#C000C0",
113 | "user-unrated", // "#000000",
114 | "user-gray", // "#808080",
115 | "user-brown", // "#804000",
116 | "user-green", // "#008000",
117 | "user-cyan", // "#00C0C0",
118 | "user-blue", // "#0000FF",
119 | "user-yellow", // "#C0C000",
120 | "user-orange", // "#FF8000",
121 | "user-red" // "#FF0000"
122 | ];
123 | }
124 |
125 | getLevel(rating){
126 | for(let level=0; level{user_screen_name}
147 | );
148 | }
149 | }
150 |
151 | const rating = new Rating();
152 |
153 | const countries = {
154 | "AF":"Afghanistan","AL":"Albania","DZ":"Algeria","AD":"Andorra","AO":"Angola","AG":"Antigua and Barbuda","AR":"Argentina","AM":"Armenia","AU":"Australia","AT":"Austria","AZ":"Azerbaijan","BS":"Bahamas","BH":"Bahrain","BD":"Bangladesh","BB":"Barbados","BY":"Belarus","BE":"Belgium","BZ":"Belize","BJ":"Benin","BT":"Bhutan","BO":"Bolivia","BA":"Bosnia and Herzegovina","BW":"Botswana","BR":"Brazil","BN":"Brunei","BG":"Bulgaria","BF":"Burkina Faso","BI":"Burundi","KH":"Cambodia","CM":"Cameroon","CA":"Canada","CV":"Cape Verde","CF":"Central African Republic","TD":"Chad","CL":"Chile","CN":"China","CO":"Colombia","KM":"Comoros","CK":"Cook","CR":"Costa Rica","HR":"Croatia","CU":"Cuba","CY":"Cyprus","CZ":"Czech Republic","CI":"Côte d\'Ivoire","CD":"Democratic Republic of the Congo","DK":"Denmark","DJ":"Djibouti","DM":"Dominica","DO":"Dominican Republic","EC":"Ecuador","EG":"Egypt","SV":"El Salvador","GQ":"Equatorial Guinea","ER":"Eritrea","EE":"Estonia","ET":"Ethiopia","FJ":"Fiji","FI":"Finland","MK":"Former Yugoslav Republic of Macedonia","FR":"France","GA":"Gabon","GM":"Gambia","GE":"Georgia","DE":"Germany","GH":"Ghana","GR":"Greece","GD":"Grenada","GT":"Guatemala","GN":"Guinea","GW":"Guinea-Bissau","GY":"Guyana","HK":"Hong Kong","HT":"Haiti","HN":"Honduras","HU":"Hungary","IS":"Iceland","IN":"India","ID":"Indonesia","IR":"Iran","IQ":"Iraq","IE":"Ireland","IL":"Israel","IT":"Italy","JM":"Jamaica","JP":"Japan","JO":"Jordan","KZ":"Kazakhstan","KE":"Kenya","KI":"Kiribati","KW":"Kuwait","KG":"Kyrgyz Republic","LA":"Laos","LV":"Latvia","LB":"Lebanon","LS":"Lesotho","LR":"Liberia","LY":"Libya","LI":"Liechtenstein","LT":"Lithuania","LU":"Luxembourg","MG":"Madagascar","MW":"Malawi","MY":"Malaysia","MV":"Maldives","ML":"Mali","MT":"Malta","MH":"Marshall","MR":"Mauritania","MU":"Mauritius","MX":"Mexico","FM":"Micronesia","MD":"Moldova","MC":"Monaco","MN":"Mongolia","ME":"Montenegro","MA":"Morocco","MZ":"Mozambique","MM":"Myanmar","NA":"Namibia","NR":"Nauru","NP":"Nepal","NL":"Netherlands","NZ":"New Zealand","NI":"Nicaragua","NE":"Niger","NG":"Nigeria","NU":"Niue","NO":"Norway","OM":"Oman","PK":"Pakistan","PW":"Palau","PS":"Palestine","PA":"Panama","PG":"Papua New Guinea","PY":"Paraguay","PE":"Peru","PH":"Philippines","PL":"Poland","PT":"Portugal","QA":"Qatar","CG":"Republic of Congo","KR":"Republic of Korea","ZA":"Republic of South Africa","RO":"Romania","RU":"Russia","RW":"Rwanda","KN":"Saint Christopher and Nevis","LC":"Saint Lucia","VC":"Saint Vincent","WS":"Samoa","SM":"San Marino","ST":"Sao Tome and Principe","SA":"Saudi Arabia","SN":"Senegal","RS":"Serbia","SC":"Seychelles","SL":"Sierra Leone","SG":"Singapore","SK":"Slovakia","SI":"Slovenia","SB":"Solomon","SO":"Somalia","SS":"South Sudan","ES":"Spain","LK":"Sri Lanka","SD":"Sudan","SR":"Suriname","SZ":"Swaziland","SE":"Sweden","CH":"Switzerland","SY":"Syria","TW":"Taiwan","TJ":"Tajikistan","TZ":"Tanzania","TH":"Thailand","TL":"Timor-Leste","TG":"Togo","TO":"Tonga","TT":"Trinidad and Tobago","TN":"Tunisia","TR":"Turkey","TM":"Turkmenistan","TV":"Tuvalu","UG":"Uganda","UA":"Ukraine","AE":"United Arab Emirates","GB":"United Kingdom","US":"United States of America","XX":"Unknown","UY":"Uruguay","UZ":"Uzbekistan","VU":"Vanuatu","VA":"Vatican","VE":"Venezuela","VN":"Viet Nam","YE":"Yemen","ZM":"Zambia","ZW":"Zimbabwe"
155 | };
156 |
157 | export {getStandings, getSortingFunction, rating, countries};
--------------------------------------------------------------------------------
/userscript_header.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name AtCoderCustomStandings
3 | // @namespace koyumeishi_scripts_AtCoderCustomStandings
4 | // @description customize your standings on atcoder
5 | // @author koyumeishi
6 | // @include http://*.contest.atcoder.jp/standings*
7 | // @include https://*.contest.atcoder.jp/standings*
8 | // @downloadURL https://koyumeishi.github.io/atcoder_script/atcoder_custom_standings.user.js
9 | // @version __version__
10 | // @run-at document-idle
11 | // @require https://unpkg.com/react@15/dist/react.js
12 | // @require https://unpkg.com/react-dom@15/dist/react-dom.js
13 | // @require https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js
14 | // @require https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.5.0/Chart.min.js
15 | // @resource materialIcons https://fonts.googleapis.com/icon?family=Material+Icons
16 | // @grant GM_getValue
17 | // @grant GM_setValue
18 | // @grant GM_listValues
19 | // @grant GM_deleteValue
20 | // @grant GM_getResourceText
21 | // @grant GM_addStyle
22 | // ==/UserScript==
23 |
24 | // LICENSE
25 | // MIT
26 |
27 | const accsVersion = "__version__";
28 |
29 | console.log( "AtCoderCustomStandings ver.", accsVersion);
30 | GM_listValues().forEach( (v) => {console.log( v, GM_getValue(v) );} );
31 |
32 | GM_addStyle( GM_getResourceText('materialIcons') );
33 |
34 |
--------------------------------------------------------------------------------