├── .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 | > ![image1](img/img1.png) 44 | 45 | --- 46 | Stats 47 | > ![image2](img/img2.png) 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 |

☆AtCoder社に一言お願いします! — モーダル閉じるの、もーだるい! http://t.co/22Y32U6dQd

— nico_shindannin (@nico_shindannin) 2014, 9月 22
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 | > ![image1](img/img1.png) 36 | 37 | --- 38 | * Statistics 39 | > ![image2](img/img2.png) 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 (); 39 | }); 40 | return ( 41 |
42 |
this.update( {"filterByCountry": !this.props.settings.filterByCountry} )} 45 | > 46 | Country 47 |
48 |
49 | 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 (
      {res}
    ); 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 |
    this.props.updateFunc() }> 11 | 12 | refreshUpdate 13 | 14 |
    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 | updateAuto (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 | 28 |
    29 | ); 30 | } 31 | 32 | generateFriendsListForm(){ 33 | const friends = this.props.friends.getList().map( (name) => { 34 | return (); 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 | 56 |
    57 |
    58 | 61 | 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 78 | }); 79 | return ( 80 |
    81 | Page Size 82 | 86 |
    87 | ); 88 | })(); 89 | 90 | const displayNameStyle = ( 91 |
    92 | Display Name Style 93 | 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 |
    37 | TwitterID : 38 | 39 | {this.props.row.twitter_id} 40 | 41 |
    ); 42 | 43 | return ( 44 |
    46 | 51 |
    52 | {submissions} 53 |
    54 |
    55 | Rating : {this.props.row.rating} 56 |
    57 |
    58 | Competitions : {this.props.row.competitions} 59 |
    60 |
    61 | Country : 62 | {countries[this.props.row.country]} 63 |
    64 | {twitter} 65 |
    68 | {friend} 69 |
    70 |
    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 |
      47 | {tab} 48 |
    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 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | {comp} 80 | 81 |
    RatingTopRankScore (Penalty)Time (Penalty)
    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 | 158 | 162 | 166 | 170 | 174 | 175 | 176 | 177 | {comp} 178 | 179 |
    { 155 | if( this.state.sortingKey == "rank" ) this.setState ({ sortingKey : "rank", ascending : !this.state.ascending }); 156 | else this.setState ({ sortingKey : "rank", ascending : true }); 157 | }}>Rank{ 159 | if( this.state.sortingKey == "country" ) this.setState ({ sortingKey : "country", ascending : !this.state.ascending }); 160 | else this.setState ({ sortingKey : "country", ascending : true }); 161 | }}>Country{ 163 | if( this.state.sortingKey == "name" ) this.setState ({ sortingKey : "name", ascending : !this.state.ascending }); 164 | else this.setState ({ sortingKey : "name", ascending : true }); 165 | }}>Top{ 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){ 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)
    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 | 242 | 246 | 247 | 248 | 249 | {comp} 250 | 251 |
    { 239 | if( this.state.sortingKey == "rating" ) this.setState ({ sortingKey : "rating", ascending : !this.state.ascending }); 240 | else this.setState ({ sortingKey : "rating", ascending : false }); 241 | }}>Rating{ 243 | if( this.state.sortingKey == "contestants" ) this.setState ({ sortingKey : "contestants", ascending : !this.state.ascending }); 244 | else this.setState ({ sortingKey : "contestants", ascending : false }); 245 | }}>Contestants
    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 | 320 | 324 | 325 | 326 | 327 | {comp} 328 | 329 |
    { 317 | if( this.state.sortingKey == "country" ) this.setState ({ sortingKey : "country", ascending : !this.state.ascending }); 318 | else this.setState ({ sortingKey : "country", ascending : true }); 319 | }}>Country{ 321 | if( this.state.sortingKey == "contestants" ) this.setState ({ sortingKey : "contestants", ascending : !this.state.ascending }); 322 | else this.setState ({ sortingKey : "contestants", ascending : false }); 323 | }}>Contestants
    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 | 221 | 222 | 223 | 224 | {/**/} 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | {rowAll} 234 | 235 |
    RatingACAttemptedSubmissionsAC / SubmissionsAC / AttemptedAC / ContestantsFastestAverage TimeAverage WA
    236 |
    237 |

    AC Time Distribution

    238 | 240 |
    241 |
    242 |

    Color Stats

    243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | {/**/} 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | {rowColor} 260 | 261 |
    RatingACAttemptedSubmissionsAC / SubmissionsAC / AttemptedAC / ContestantsFastestAverage TimeAverage WA
    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 | --------------------------------------------------------------------------------