├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── components │ ├── molecules │ │ ├── VChart.vue │ │ └── VHeading.vue │ ├── organisms │ │ ├── ChartPane.vue │ │ ├── MainPageHeader.vue │ │ └── UserNameInput.vue │ └── pages │ │ └── MainPage.vue ├── libs │ └── rating-chart.ts ├── main.ts ├── router.ts ├── shims-tsx.d.ts ├── shims-vue.d.ts ├── styles │ └── simple-reset.css ├── types │ ├── contest-history.ts │ ├── contest-information.ts │ └── submission.ts └── vuex │ ├── modules │ ├── contest-history.ts │ ├── contest-information.ts │ └── submission.ts │ └── store.ts └── tsconfig.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | trim_trailing_whitespace = true 6 | insert_final_newline = true 7 | max_line_length = 100 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: ['plugin:vue/essential', '@vue/airbnb', '@vue/typescript'], 7 | rules: { 8 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 9 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 10 | semi: 0, 11 | 'class-methods-use-this': 0, 12 | 'object-curly-newline': 0, 13 | 'import/prefer-default-export': 0, 14 | 'max-len': [1, 120], 15 | 'comma-dangle': 0, 16 | }, 17 | parserOptions: { 18 | parser: '@typescript-eslint/parser', 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "trailingComma": "es5", 4 | "tabWidth": 2, 5 | "semi": false, 6 | "singleQuote": true 7 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 havveFn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # atcoder-charts 2 | 3 | [atcoder-charts](https://atcoder-charts.netlify.com/) 4 | 5 | > no longer maintained. 6 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app', 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "atcoder-charts", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@types/chart.js": "^2.7.56", 12 | "axios": "^0.19.0", 13 | "chart.js": "^2.8.0", 14 | "chartjs-plugin-annotation": "^0.5.7", 15 | "core-js": "^2.6.5", 16 | "pug": "^2.0.4", 17 | "pug-loader": "^2.4.0", 18 | "pug-plain-loader": "^1.0.0", 19 | "vue": "^2.6.10", 20 | "vue-class-component": "^7.0.2", 21 | "vue-property-decorator": "^8.1.0", 22 | "vue-router": "^3.0.3", 23 | "vuex": "^3.1.1", 24 | "vuex-module-decorators": "^0.9.9" 25 | }, 26 | "devDependencies": { 27 | "@vue/cli-plugin-babel": "^3.9.0", 28 | "@vue/cli-plugin-eslint": "^3.9.0", 29 | "@vue/cli-plugin-typescript": "^3.9.0", 30 | "@vue/cli-service": "^3.9.0", 31 | "@vue/eslint-config-airbnb": "^4.0.0", 32 | "@vue/eslint-config-typescript": "^4.0.0", 33 | "babel-eslint": "^10.0.1", 34 | "eslint": "^5.16.0", 35 | "eslint-plugin-vue": "^5.0.0", 36 | "sass": "^1.18.0", 37 | "sass-loader": "^7.1.0", 38 | "typescript": "^3.4.3", 39 | "vue-template-compiler": "^2.6.10" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tars0x9752/atcoder-charts/f0c7023616437bfcc583aa1030fce72cff9736d5/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | AtCoder Charts 11 | 12 | 13 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 19 | 20 | 48 | -------------------------------------------------------------------------------- /src/components/molecules/VChart.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 60 | 61 | 67 | -------------------------------------------------------------------------------- /src/components/molecules/VHeading.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 24 | 25 | 45 | -------------------------------------------------------------------------------- /src/components/organisms/ChartPane.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 35 | 36 | 50 | -------------------------------------------------------------------------------- /src/components/organisms/MainPageHeader.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 29 | 30 | 79 | -------------------------------------------------------------------------------- /src/components/organisms/UserNameInput.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 136 | 137 | 310 | -------------------------------------------------------------------------------- /src/components/pages/MainPage.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 91 | 92 | 102 | -------------------------------------------------------------------------------- /src/libs/rating-chart.ts: -------------------------------------------------------------------------------- 1 | import 'chartjs-plugin-annotation' 2 | import { ChartData, ChartOptions } from 'chart.js' 3 | import { ContestHistory } from '@/types/contest-history' 4 | 5 | export const createRatingChart = (contestHistory: ContestHistory) => { 6 | const ratedList = contestHistory.filter(contestResult => contestResult.isRated) 7 | 8 | const ratings = ratedList.map(rated => rated.newRating) 9 | 10 | const performances = ratedList.map(rated => rated.performance) 11 | 12 | const labels = ratedList.map(rated => `${rated.contestName}`) 13 | 14 | const data: ChartData = { 15 | labels, 16 | datasets: [ 17 | { 18 | label: 'Rating', 19 | type: 'line', 20 | data: ratings, 21 | fill: false, 22 | lineTension: 0, 23 | backgroundColor: '#00a9a5', 24 | borderColor: '#00a9a5', 25 | borderWidth: 2, 26 | }, 27 | { 28 | label: 'Performance', 29 | type: 'line', 30 | data: performances, 31 | fill: false, 32 | lineTension: 0.3, 33 | backgroundColor: '#00a9a5CC', 34 | borderColor: '#00a9a5CC', 35 | borderWidth: 1, 36 | borderDash: [10, 5], 37 | pointStyle: 'rectRot', 38 | pointRadius: 3, 39 | pointBackgroundColor: '#fff', 40 | }, 41 | ], 42 | } 43 | 44 | const createRegion = ( 45 | yMin: number, 46 | yMax: number, 47 | backgroundColor: string, 48 | borderColor: string 49 | ) => ({ 50 | type: 'box', 51 | xScaleID: 'x0', 52 | yScaleID: 'y0', 53 | xMin: labels[0], 54 | xMax: labels[labels.length - 1], 55 | yMin, 56 | yMax, 57 | backgroundColor, 58 | borderColor, 59 | borderWidth: 0, 60 | }) 61 | 62 | const brownRegion = createRegion(400, 799, '#AF81691A', '#AF816900') 63 | 64 | const grennRegion = createRegion(800, 1199, '#419D781A', '#419D7800') 65 | 66 | const lightBlueRegion = createRegion(1200, 1599, '#29CCF41A', '#29CCF400') 67 | 68 | const blueRegion = createRegion(1600, 1999, '#266DD31A', '#266DD300') 69 | 70 | const yellowRegion = createRegion(2000, 2399, '#FFED4C1A', '#FFED4C00') 71 | 72 | const orangeRegion = createRegion(2400, 2799, '#FF90001A', '#FF900000') 73 | 74 | const redRegion = createRegion(2800, 9999, '#ED254E1A', '#ED254E00') 75 | 76 | const options: ChartOptions = { 77 | annotation: { 78 | drawTime: 'beforeDatasetsDraw', 79 | annotations: [ 80 | brownRegion, 81 | grennRegion, 82 | lightBlueRegion, 83 | blueRegion, 84 | yellowRegion, 85 | orangeRegion, 86 | redRegion, 87 | ], 88 | }, 89 | responsive: true, 90 | maintainAspectRatio: true, 91 | legend: { 92 | display: false, 93 | }, 94 | tooltips: { 95 | displayColors: false, 96 | callbacks: { 97 | afterLabel: tooltipItem => labels[tooltipItem.index!], 98 | }, 99 | }, 100 | hover: { 101 | mode: 'point', 102 | }, 103 | scales: { 104 | xAxes: [ 105 | { 106 | id: 'x0', 107 | ticks: { 108 | callback: (_, i) => `#${i + 1}`, 109 | }, 110 | }, 111 | ], 112 | yAxes: [ 113 | { 114 | id: 'y0', 115 | }, 116 | ], 117 | }, 118 | } as ChartOptions 119 | 120 | return { 121 | ratingChartData: data, 122 | ratingChartOptions: options, 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from '@/App.vue' 3 | import router from '@/router' 4 | import store from '@/vuex/store' 5 | 6 | Vue.config.productionTip = false 7 | 8 | new Vue({ 9 | router, 10 | store, 11 | render: h => h(App), 12 | }).$mount('#app') 13 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import MainPage from '@/components/pages/MainPage.vue' 4 | 5 | const Path = { 6 | MainPage: '/', 7 | } 8 | 9 | Vue.use(Router) 10 | 11 | export default new Router({ 12 | routes: [ 13 | { 14 | path: Path.MainPage, 15 | component: MainPage, 16 | }, 17 | { 18 | path: '*', 19 | component: MainPage, 20 | }, 21 | ], 22 | }) 23 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue'; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | 4 | export default Vue; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles/simple-reset.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | font-size: 16px; 4 | } 5 | 6 | *, 7 | *:before, 8 | *:after { 9 | box-sizing: inherit; 10 | } 11 | 12 | body, 13 | div, 14 | h1, 15 | h2, 16 | h3, 17 | h4, 18 | h5, 19 | h6, 20 | p, 21 | span, 22 | ol, 23 | ul, 24 | input { 25 | margin: 0; 26 | padding: 0; 27 | outline: 0; 28 | font-weight: normal; 29 | } 30 | 31 | a { 32 | text-decoration: none; 33 | } 34 | 35 | ol, 36 | ul { 37 | list-style: none; 38 | } 39 | 40 | img { 41 | max-width: 100%; 42 | height: auto; 43 | } 44 | -------------------------------------------------------------------------------- /src/types/contest-history.ts: -------------------------------------------------------------------------------- 1 | export interface RawContestResult { 2 | IsRated: boolean 3 | Place: number 4 | OldRating: number 5 | NewRating: number 6 | Performance: number 7 | InnerPerformance: number 8 | ContestScreenName: string 9 | ContestName: string 10 | EndTime: string 11 | } 12 | 13 | export interface ContestResult { 14 | isRated: boolean 15 | place: number 16 | oldRating: number 17 | newRating: number 18 | performance: number 19 | innerPerformance: number 20 | contestScreenName: string 21 | contestName: string 22 | endTime: string 23 | } 24 | 25 | export type RawContestHistory = RawContestResult[] 26 | 27 | export type ContestHistory = ContestResult[] 28 | -------------------------------------------------------------------------------- /src/types/contest-information.ts: -------------------------------------------------------------------------------- 1 | /* eslint camelcase: 0 */ 2 | 3 | export interface ContestInformation { 4 | id: string 5 | title: string 6 | start_epoch_second: number 7 | rate_change: string 8 | duration_second: number 9 | } 10 | -------------------------------------------------------------------------------- /src/types/submission.ts: -------------------------------------------------------------------------------- 1 | /* eslint camelcase: 0 */ 2 | 3 | export interface Submission { 4 | id: number 5 | epoch_second: number 6 | problem_id: string 7 | contest_id: string 8 | user_id: string 9 | language: string 10 | point: number 11 | length: number 12 | result: Result 13 | execution_time: number 14 | } 15 | 16 | export enum Result { 17 | AC = 'AC', 18 | WA = 'WA', 19 | TLE = 'TLE', 20 | CE = 'CE', 21 | RE = 'RE', 22 | } 23 | -------------------------------------------------------------------------------- /src/vuex/modules/contest-history.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Module, VuexModule, Mutation, Action, getModule } from 'vuex-module-decorators' 3 | import store from '@/vuex/store' 4 | import { 5 | RawContestResult, 6 | ContestResult, 7 | ContestHistory, 8 | RawContestHistory, 9 | } from '@/types/contest-history' 10 | 11 | const netlifyFunctions = 'https://havefn-atcoder-api.netlify.app/.netlify/functions/' 12 | 13 | const httpClient = axios.create({ 14 | baseURL: netlifyFunctions, 15 | }) 16 | 17 | const formatRawData = (raw: RawContestResult) => { 18 | const { 19 | IsRated, 20 | Place, 21 | OldRating, 22 | NewRating, 23 | Performance, 24 | InnerPerformance, 25 | ContestScreenName, 26 | ContestName, 27 | EndTime, 28 | } = raw 29 | 30 | const camel: ContestResult = { 31 | isRated: IsRated, 32 | place: Place, 33 | oldRating: OldRating, 34 | newRating: NewRating, 35 | performance: Performance, 36 | innerPerformance: InnerPerformance, 37 | contestScreenName: ContestScreenName, 38 | contestName: ContestName, 39 | endTime: EndTime, 40 | } 41 | 42 | return camel 43 | } 44 | 45 | @Module({ dynamic: true, name: 'ContestHistoryModule', store, namespaced: true }) 46 | class ContestHistoryModule extends VuexModule { 47 | private contestHistory: ContestHistory | null = null 48 | 49 | get getContestHistory(): ContestHistory | null { 50 | return this.contestHistory 51 | } 52 | 53 | @Mutation 54 | setContestHistory(contestHistory: ContestHistory | null) { 55 | this.contestHistory = contestHistory 56 | } 57 | 58 | @Action({ rawError: true }) 59 | async fetchContestHistory(username: string) { 60 | if (!username) { 61 | throw new Error('username required') 62 | } 63 | 64 | const res = await httpClient.get(`/history?user=${username}`) 65 | 66 | const rawContestHistory: RawContestHistory = res.data 67 | 68 | const contestHistory: ContestHistory = rawContestHistory.map(formatRawData) 69 | 70 | this.setContestHistory(contestHistory) 71 | } 72 | } 73 | 74 | export const contestHistoryModule = getModule(ContestHistoryModule) 75 | -------------------------------------------------------------------------------- /src/vuex/modules/contest-information.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Module, VuexModule, Mutation, Action, getModule } from 'vuex-module-decorators' 3 | import store from '@/vuex/store' 4 | import { ContestInformation } from '@/types/contest-information' 5 | 6 | const httpClient = axios.create({ 7 | baseURL: 'https://kenkoooo.com/atcoder/resources/contests.json', 8 | }) 9 | 10 | @Module({ dynamic: true, name: 'ContestInformationModule', store, namespaced: true }) 11 | class ContestInformationModule extends VuexModule { 12 | private contestInformations: ContestInformation[] | null = null 13 | 14 | get getContestInformations(): ContestInformation[] | null { 15 | return this.contestInformations 16 | } 17 | 18 | @Mutation 19 | setContestInformations(contestInformations: ContestInformation[] | null) { 20 | this.contestInformations = contestInformations 21 | } 22 | 23 | @Action({ rawError: true }) 24 | async fetchContestInformation() { 25 | const res = await httpClient.get('/') 26 | 27 | const contestInformations = res.data 28 | 29 | this.setContestInformations(contestInformations) 30 | } 31 | } 32 | 33 | export const contestInformationModule = getModule(ContestInformationModule) 34 | -------------------------------------------------------------------------------- /src/vuex/modules/submission.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Module, VuexModule, Mutation, Action, getModule } from 'vuex-module-decorators' 3 | import store from '@/vuex/store' 4 | import { Submission } from '@/types/submission' 5 | 6 | const httpClient = axios.create({ 7 | baseURL: 'https://kenkoooo.com/atcoder/atcoder-api', 8 | }) 9 | 10 | @Module({ dynamic: true, name: 'SubmissionModule', store, namespaced: true }) 11 | class SubmissionModule extends VuexModule { 12 | private submissions: Submission[] | null = null 13 | 14 | get getSubmissions(): Submission[] | null { 15 | return this.submissions 16 | } 17 | 18 | @Mutation 19 | setSubmissions(submissions: Submission[] | null) { 20 | this.submissions = submissions 21 | } 22 | 23 | @Action({ rawError: true }) 24 | async fetchSubmissions(username: string) { 25 | if (!username) { 26 | throw new Error('username required') 27 | } 28 | 29 | const res = await httpClient.get('/results', { 30 | params: { 31 | user: username, 32 | }, 33 | }) 34 | 35 | const submissions: Submission[] = res.data 36 | 37 | this.setSubmissions(submissions) 38 | } 39 | } 40 | 41 | export const submissionModule = getModule(SubmissionModule) 42 | -------------------------------------------------------------------------------- /src/vuex/store.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | Vue.use(Vuex) 5 | 6 | type RootState = object 7 | 8 | export { RootState } 9 | 10 | export default new Vuex.Store({}) 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env" 16 | ], 17 | "paths": { 18 | "@/*": [ 19 | "src/*" 20 | ] 21 | }, 22 | "lib": [ 23 | "esnext", 24 | "dom", 25 | "dom.iterable", 26 | "scripthost" 27 | ] 28 | }, 29 | "include": [ 30 | "src/**/*.ts", 31 | "src/**/*.tsx", 32 | "src/**/*.vue", 33 | "tests/**/*.ts", 34 | "tests/**/*.tsx" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | --------------------------------------------------------------------------------