├── src ├── __init__.py ├── build │ └── _placeholder ├── analytics_league │ ├── __init__.py │ ├── utils.py │ └── history.py ├── static │ ├── js │ │ ├── impact_summary.js │ │ ├── test.js │ │ ├── history.js │ │ ├── top_squads.js │ │ ├── load.js │ │ ├── who_played.js │ │ ├── weekly.js │ │ ├── ownership_trend.js │ │ ├── manager_form.js │ │ ├── ownership_rates.js │ │ └── sampling_utils.js │ ├── asset │ │ ├── logo.png │ │ ├── logo-mini.png │ │ ├── apple_logo.png │ │ ├── apple_splash.png │ │ ├── apple_logo_114.png │ │ ├── apple_splash_320.png │ │ └── manifest.json │ ├── images │ │ ├── blog1.jpg │ │ ├── blog2.jpg │ │ ├── blog3.jpg │ │ ├── blog4.jpg │ │ ├── blog5.jpg │ │ ├── blog6.jpg │ │ ├── ffhub.png │ │ ├── repo.jpg │ │ ├── review.png │ │ ├── logo_128.png │ │ ├── logo_400.png │ │ ├── podcast.png │ │ ├── clubs │ │ │ ├── ARS.png │ │ │ ├── AVL.png │ │ │ ├── BHA.png │ │ │ ├── BRE.png │ │ │ ├── BUR.png │ │ │ ├── CHE.png │ │ │ ├── CRY.png │ │ │ ├── EVE.png │ │ │ ├── FUL.png │ │ │ ├── LEE.png │ │ │ ├── LEI.png │ │ │ ├── LIV.png │ │ │ ├── MCI.png │ │ │ ├── MUN.png │ │ │ ├── NEW.png │ │ │ ├── NOR.png │ │ │ ├── SHU.png │ │ │ ├── SOU.png │ │ │ ├── TOT.png │ │ │ ├── TOT1.png │ │ │ ├── WAT.png │ │ │ ├── WBA.png │ │ │ ├── WHU.png │ │ │ └── WOL.png │ │ ├── logo_400_tr.png │ │ ├── youtube_1.jpg │ │ ├── youtube_2.jpg │ │ ├── youtube_3.jpg │ │ ├── loading.svg │ │ ├── field.svg │ │ └── jersey.svg │ ├── json │ │ ├── top_managers.tsv │ │ └── fpl_analytics.json │ ├── extra │ │ └── jersey_buttons.txt │ ├── csv │ │ └── team.csv │ └── css │ │ └── svg.css ├── static-values.json ├── freezer.py ├── sample.py ├── run.py ├── cache_api.py ├── templates │ ├── test.html │ ├── signup.html │ ├── ownership_trend.html │ ├── history.html │ ├── footer.html │ ├── spirit_team.html │ ├── top_squads.html │ ├── load.html │ ├── manager_form.html │ └── impact_summary.html ├── encrypt.py └── prep.py ├── scripts ├── sample.sh ├── update_league.sh └── requirements.txt ├── .gitignore ├── archive ├── Dockerfile-custom ├── Dockerfile-sampler ├── refresh_league.py ├── fpl_analytics_league.yml ├── highlights-fdr-parts.html └── fpl_analytics_league.html ├── Dockerfile ├── Dockerfile-dev ├── docker-compose.yml ├── custom-request.yml ├── dev-compose.yml └── .github └── workflows ├── analytics_xp_league.yml ├── sample_fpl.yml ├── main.yml └── regenerate_element_gameweek.yml /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/build/_placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/analytics_league/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/sample.sh: -------------------------------------------------------------------------------- 1 | cd ../src 2 | python3 sample.py 3 | -------------------------------------------------------------------------------- /src/static/js/impact_summary.js: -------------------------------------------------------------------------------- 1 | console.log("Yay") -------------------------------------------------------------------------------- /scripts/update_league.sh: -------------------------------------------------------------------------------- 1 | cd ../src 2 | python3 xp_league.py 3 | -------------------------------------------------------------------------------- /scripts/requirements.txt: -------------------------------------------------------------------------------- 1 | sasoptpy>=1.0.5a0 2 | pandas 3 | aiohttp 4 | asyncio 5 | -------------------------------------------------------------------------------- /src/static/asset/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/asset/logo.png -------------------------------------------------------------------------------- /src/static/images/blog1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/blog1.jpg -------------------------------------------------------------------------------- /src/static/images/blog2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/blog2.jpg -------------------------------------------------------------------------------- /src/static/images/blog3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/blog3.jpg -------------------------------------------------------------------------------- /src/static/images/blog4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/blog4.jpg -------------------------------------------------------------------------------- /src/static/images/blog5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/blog5.jpg -------------------------------------------------------------------------------- /src/static/images/blog6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/blog6.jpg -------------------------------------------------------------------------------- /src/static/images/ffhub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/ffhub.png -------------------------------------------------------------------------------- /src/static/images/repo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/repo.jpg -------------------------------------------------------------------------------- /src/static/images/review.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/review.png -------------------------------------------------------------------------------- /src/static/asset/logo-mini.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/asset/logo-mini.png -------------------------------------------------------------------------------- /src/static/images/logo_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/logo_128.png -------------------------------------------------------------------------------- /src/static/images/logo_400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/logo_400.png -------------------------------------------------------------------------------- /src/static/images/podcast.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/podcast.png -------------------------------------------------------------------------------- /src/static/asset/apple_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/asset/apple_logo.png -------------------------------------------------------------------------------- /src/static/asset/apple_splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/asset/apple_splash.png -------------------------------------------------------------------------------- /src/static/images/clubs/ARS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/ARS.png -------------------------------------------------------------------------------- /src/static/images/clubs/AVL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/AVL.png -------------------------------------------------------------------------------- /src/static/images/clubs/BHA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/BHA.png -------------------------------------------------------------------------------- /src/static/images/clubs/BRE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/BRE.png -------------------------------------------------------------------------------- /src/static/images/clubs/BUR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/BUR.png -------------------------------------------------------------------------------- /src/static/images/clubs/CHE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/CHE.png -------------------------------------------------------------------------------- /src/static/images/clubs/CRY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/CRY.png -------------------------------------------------------------------------------- /src/static/images/clubs/EVE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/EVE.png -------------------------------------------------------------------------------- /src/static/images/clubs/FUL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/FUL.png -------------------------------------------------------------------------------- /src/static/images/clubs/LEE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/LEE.png -------------------------------------------------------------------------------- /src/static/images/clubs/LEI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/LEI.png -------------------------------------------------------------------------------- /src/static/images/clubs/LIV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/LIV.png -------------------------------------------------------------------------------- /src/static/images/clubs/MCI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/MCI.png -------------------------------------------------------------------------------- /src/static/images/clubs/MUN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/MUN.png -------------------------------------------------------------------------------- /src/static/images/clubs/NEW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/NEW.png -------------------------------------------------------------------------------- /src/static/images/clubs/NOR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/NOR.png -------------------------------------------------------------------------------- /src/static/images/clubs/SHU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/SHU.png -------------------------------------------------------------------------------- /src/static/images/clubs/SOU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/SOU.png -------------------------------------------------------------------------------- /src/static/images/clubs/TOT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/TOT.png -------------------------------------------------------------------------------- /src/static/images/clubs/TOT1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/TOT1.png -------------------------------------------------------------------------------- /src/static/images/clubs/WAT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/WAT.png -------------------------------------------------------------------------------- /src/static/images/clubs/WBA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/WBA.png -------------------------------------------------------------------------------- /src/static/images/clubs/WHU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/WHU.png -------------------------------------------------------------------------------- /src/static/images/clubs/WOL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/clubs/WOL.png -------------------------------------------------------------------------------- /src/static/images/logo_400_tr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/logo_400_tr.png -------------------------------------------------------------------------------- /src/static/images/youtube_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/youtube_1.jpg -------------------------------------------------------------------------------- /src/static/images/youtube_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/youtube_2.jpg -------------------------------------------------------------------------------- /src/static/images/youtube_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/images/youtube_3.jpg -------------------------------------------------------------------------------- /src/static/json/top_managers.tsv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/json/top_managers.tsv -------------------------------------------------------------------------------- /src/static/asset/apple_logo_114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/asset/apple_logo_114.png -------------------------------------------------------------------------------- /src/static/asset/apple_splash_320.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sertalpbilal/fpl_optimized/HEAD/src/static/asset/apple_splash_320.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.log 3 | *.pyc 4 | .vscode/* 5 | setup_gh.cmd 6 | src/build 7 | secret.key 8 | fplreview.py 9 | simulator.py 10 | *.tmp.* 11 | 12 | -------------------------------------------------------------------------------- /archive/Dockerfile-custom: -------------------------------------------------------------------------------- 1 | FROM sertalpbilal/coin-or-optimization-with-batteries:latest 2 | 3 | COPY src /app 4 | WORKDIR /app 5 | 6 | CMD python3 -u custom.py 7 | -------------------------------------------------------------------------------- /archive/Dockerfile-sampler: -------------------------------------------------------------------------------- 1 | FROM sertalpbilal/coin-or-optimization-with-batteries:latest 2 | 3 | COPY src /app 4 | WORKDIR /app 5 | 6 | CMD python3 -u sample.py 7 | -------------------------------------------------------------------------------- /src/static-values.json: -------------------------------------------------------------------------------- 1 | { 2 | "season": "2025-26", 3 | "season_status": "live", 4 | "win_driver": "C:\\work\\chromedriver.exe", 5 | "unix_driver": "/usr/local/bin/chromedriver" 6 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM sertalpbilal/coin-or-optimization-with-batteries:latest 2 | 3 | RUN pip install cryptography pycryptodome aiohttp asyncio 4 | COPY src /app 5 | WORKDIR /app 6 | 7 | CMD python3 -u run.py 8 | -------------------------------------------------------------------------------- /src/freezer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from flask_frozen import Freezer 4 | from app import app, list_all_snapshots 5 | 6 | def freeze_all(): 7 | freezer = Freezer(app) 8 | freezer.freeze() 9 | -------------------------------------------------------------------------------- /Dockerfile-dev: -------------------------------------------------------------------------------- 1 | FROM sertalpbilal/coin-or-optimization-with-batteries:latest 2 | 3 | RUN pip install cryptography pycryptodome aiohttp asyncio 4 | COPY src /app 5 | WORKDIR /app 6 | 7 | CMD python3 -u app.py 8 | -------------------------------------------------------------------------------- /src/sample.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from collect import sample_fpl_teams 4 | # from collect import create_folders 5 | import os 6 | 7 | if __name__ == "__main__": 8 | gw = os.environ.get('GW', None) 9 | sample_fpl_teams(gw) 10 | -------------------------------------------------------------------------------- /archive/refresh_league.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | from collect import create_folders, get_fpl_analytics_league 4 | 5 | if __name__ == "__main__": 6 | input_folder, output_folder, season_folder = create_folders() 7 | get_fpl_analytics_league(input_folder) 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | fpl_aa_generator: 4 | container_name: fpl_aa 5 | build: . 6 | environment: 7 | - PYTHONUNBUFFERED=1 8 | - LANG=C.UTF-8 9 | # env_file: 10 | # - ./user.env 11 | volumes: 12 | - ./src:/app 13 | -------------------------------------------------------------------------------- /custom-request.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | fpl_aa_web_demo: 4 | container_name: fpl_aa_dev 5 | build: 6 | context: . 7 | dockerfile: Dockerfile-custom 8 | environment: 9 | - PYTHONUNBUFFERED=1 10 | - LANG=C.UTF-8 11 | volumes: 12 | - /fpl_aa:/app 13 | -------------------------------------------------------------------------------- /dev-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | fpl_aa_web_demo: 4 | container_name: fpl_aa_dev 5 | build: 6 | context: . 7 | dockerfile: Dockerfile-dev 8 | environment: 9 | - PYTHONUNBUFFERED=1 10 | - LANG=C.UTF-8 11 | ports: 12 | - "8001:5000" 13 | volumes: 14 | - ./src:/app 15 | -------------------------------------------------------------------------------- /src/static/extra/jersey_buttons.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 🗙 8 | -------------------------------------------------------------------------------- /src/static/asset/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "dir": "ltr", 3 | "lang": "en", 4 | "name": "FPL Optimized", 5 | "display": "standalone", 6 | "start_url": "/", 7 | "scope": ".", 8 | "short_name": "FPL Optimized", 9 | "theme_color": "#494949", 10 | "description": "FPL Optimized - Tools, blogs, and tutorials about optimization in FPL", 11 | "orientation": "portrait", 12 | "background_color": "transparent", 13 | "icons": [{ 14 | "src": "./static/asset/apple_logo_114.png" 15 | }] 16 | } -------------------------------------------------------------------------------- /src/analytics_league/utils.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | import json 4 | import pathlib 5 | from collect import get_fpl_info 6 | 7 | def read_league_teams(): 8 | base_folder = pathlib.Path(__file__).parent.resolve() 9 | with open(base_folder / '../static/json/fpl_analytics.json') as f: 10 | data = json.load(f) 11 | return data 12 | 13 | def get_current_gw(): 14 | data = get_fpl_info('now') 15 | gws = [i for i in data['events'] if i['is_previous'] == True] 16 | return gws[0]['id'] 17 | 18 | def save_to_json(name, data): 19 | with open(name, 'w') as f: 20 | json.dump(data, f) 21 | 22 | -------------------------------------------------------------------------------- /src/run.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import sys 4 | 5 | from solve import solve_all 6 | from freezer import freeze_all 7 | 8 | 9 | if __name__ == "__main__": 10 | 11 | opts = '' 12 | if len(sys.argv) > 1: 13 | opts = sys.argv[1] 14 | 15 | if opts != "skip-opt": 16 | from collect import get_all_data, encrypt_files 17 | # from simulator import generate_simulations 18 | print("Step 1: Get All Data") 19 | input_folder, output_folder, next_gw = get_all_data() 20 | # if next_gw != 39: 21 | # solve_all(input_folder, output_folder) 22 | # generate_simulations(input_folder, output_folder, 100) 23 | # encrypt_files(input_folder, page='free-planner', remove=True) 24 | freeze_all() 25 | -------------------------------------------------------------------------------- /src/cache_api.py: -------------------------------------------------------------------------------- 1 | 2 | import requests 3 | import json 4 | import os 5 | 6 | BASE = "https://fantasy.premierleague.com/api/" 7 | season = "2020-21" 8 | API = f"build/data/{season}/api/" 9 | TEAM = 2221044 10 | 11 | def cache_page_as(page, address): 12 | URL = BASE + page 13 | TARGET = API + address 14 | 15 | if os.path.exists(TARGET): 16 | print("Existing target... skipping") 17 | return 18 | 19 | print(f"Requesting {URL} -> {TARGET}") 20 | r = requests.get(URL) 21 | if r.status_code != 200: 22 | print(r.status_code) 23 | print(f"Error in request {page}") 24 | return 25 | with open(TARGET, 'w') as f: 26 | json.dump(r.json(), f) 27 | 28 | cache_page_as("bootstrap-static/", "main.json") 29 | cache_page_as("fixtures/", "fixtures/all.json") 30 | for gw in range(1,39): 31 | cache_page_as(f"fixtures/?event={gw}", f"fixtures/{gw}.json") 32 | for gw in range(1,39): 33 | cache_page_as(f"event/{gw}/live", f"live/{gw}.json") 34 | cache_page_as(f"entry/{TEAM}/", "team_main.json") 35 | cache_page_as(f"entry/{TEAM}/history/", "team_history.json") 36 | cache_page_as(f"entry/{TEAM}/transfers/", "team_transfers.json") 37 | for gw in range(1,39): 38 | cache_page_as(f"entry/{TEAM}/event/{gw}/picks/", f"picks/{gw}.json") 39 | 40 | -------------------------------------------------------------------------------- /archive/fpl_analytics_league.yml: -------------------------------------------------------------------------------- 1 | name: Update FPL Analytics League 2 | 3 | on: 4 | workflow_dispatch: 5 | # schedule: 6 | # - cron: '0 7 * * *' 7 | 8 | jobs: 9 | build-and-run: 10 | name: Fetch FPLReview for Analytics League 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check current directory 15 | run: pwd 16 | - name: Checkout repository 17 | uses: actions/checkout@v2 18 | - name: Checkout webpage branch 19 | uses: actions/checkout@v2 20 | with: 21 | ref: 'webpage' 22 | path: ./build 23 | - name: Make sure data folder exists 24 | run: mkdir -p ./build/data 25 | - name: Build docker image 26 | run: docker build -f Dockerfile-scrap -t scraper . 27 | - name: Run docker image 28 | run: | 29 | docker run -t --rm -e LANG=C.UTF-8 -v $(pwd)/build:/app/build scraper bash -c "python3 refresh_league.py" 30 | - name: Add changes to the branch 31 | run: | 32 | cd build 33 | git add -u 34 | git add . 35 | git config user.name "Github Action" 36 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 37 | git commit -m "Manual Update - Analytics League $GITHUB_RUN_ID" 38 | git push 39 | -------------------------------------------------------------------------------- /src/templates/test.html: -------------------------------------------------------------------------------- 1 | "% include 'header.html' %" 2 | 3 |
4 |
5 |
6 |
7 |
Test Page
8 |
9 | 10 |

Test Page!

11 |

{{ sample_data }}

12 |

13 |

{{ data_ac }}

14 |

15 |

{{ data_fo }}

16 | 17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 |
25 | 28 |
29 |
30 |
31 | 32 |
33 | 34 | "% with scripts=["main", "test"] %" "% include 'footer.html' %" "% endwith %" -------------------------------------------------------------------------------- /.github/workflows/analytics_xp_league.yml: -------------------------------------------------------------------------------- 1 | name: Get data for Analytics xP League 2 | 3 | on: 4 | workflow_dispatch: 5 | # schedule: 6 | # - cron: '0 7 * * *' 7 | 8 | jobs: 9 | build-and-run: 10 | name: Fetch IDs and picks for Analytics xP league 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Check current directory 15 | run: pwd 16 | - name: Checkout repository 17 | uses: actions/checkout@v2 18 | - name: Checkout webpage branch 19 | uses: actions/checkout@v2 20 | with: 21 | ref: 'webpage' 22 | path: ./src/build 23 | - name: Make sure data folder exists 24 | run: mkdir -p ./src/build/data 25 | - name: Make sure statuc folder exists 26 | run: mkdir -p ./src/build/static 27 | - name: Fetch docker image 28 | run: docker pull sertalpbilal/selenium_docker:latest 29 | - name: Run docker image 30 | run: | 31 | docker run -t --rm -e LANG=C.UTF-8 -v $(pwd):/app sertalpbilal/selenium_docker bash -c "cd /app/scripts && chmod +x update_league.sh && ./update_league.sh" 32 | - name: Add changes to the branch 33 | run: | 34 | cd src/build 35 | git add -u 36 | git add . 37 | git config user.name "Github Action" 38 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 39 | git commit -m "Auto Update - Analytics xP League $GITHUB_RUN_ID" 40 | git push 41 | -------------------------------------------------------------------------------- /src/static/js/test.js: -------------------------------------------------------------------------------- 1 | 2 | var app = new Vue({ 3 | el: '#app', 4 | data: { 5 | sample_data: "test", 6 | data_ac: {source: 'ac', response_code: 0, data: ''}, 7 | data_fo: {source: 'fo', response_code: 0, data: ''} 8 | }, 9 | computed: { 10 | 11 | }, 12 | methods: { 13 | test_ac() { 14 | let proxy = "https://cors.alpscode.com" 15 | $.ajax({ 16 | type: "GET", 17 | url: `${proxy}/fantasy.premierleague.com/api/bootstrap-static/`, 18 | dataType: "json", 19 | async: true, 20 | success: data => { 21 | app.data_ac.data = data["events"][0] 22 | }, 23 | error: (e) => { 24 | console.log("Cannot get FPL main data"); 25 | console.error(e) 26 | } 27 | }); 28 | }, 29 | test_fo() { 30 | let proxy = "https://cors.fploptimized.com" 31 | $.ajax({ 32 | type: "GET", 33 | url: `${proxy}/fantasy.premierleague.com/api/bootstrap-static/`, 34 | dataType: "json", 35 | async: true, 36 | success: data => { 37 | app.data_fo.data = data["events"][0] 38 | }, 39 | error: (e) => { 40 | console.log("Cannot get FPL main data"); 41 | console.error(e) 42 | } 43 | }); 44 | } 45 | } 46 | }) 47 | 48 | 49 | $(document).ready(() => { 50 | 51 | }) 52 | -------------------------------------------------------------------------------- /src/encrypt.py: -------------------------------------------------------------------------------- 1 | from cryptography.fernet import Fernet 2 | import os 3 | import json 4 | 5 | def write_key(key_name): 6 | key = Fernet.generate_key() 7 | with open("secret.key", "w") as key_file: 8 | json.dump({key_name: key.decode()}, key_file) 9 | 10 | def load_key(key_name): 11 | with open("secret.key", "r") as key_file: 12 | keys = json.load(key_file) 13 | return keys[key_name].encode() 14 | 15 | 16 | def encrypt(filename, key_name='FERNET_KEY'): 17 | try: 18 | key = load_key(key_name) 19 | except: 20 | key = os.environ.get(key_name) 21 | f = Fernet(key) 22 | with open(filename, "rb") as file: 23 | file_data = file.read() 24 | encrypted_data = f.encrypt(file_data) 25 | with open(filename + "-encrypted", "wb") as file: 26 | file.write(encrypted_data) 27 | 28 | 29 | def decrypt(filename, key_name='FERNET_KEY'): 30 | try: 31 | key = load_key(key_name) 32 | except: 33 | key = os.environ.get(key_name) 34 | f = Fernet(key) 35 | with open(filename + "-encrypted", "rb") as file: 36 | encrypted_data = file.read() 37 | decrypted_data = f.decrypt(encrypted_data) 38 | with open(filename, "wb") as file: 39 | file.write(decrypted_data) 40 | 41 | 42 | def read_encrypted(filename, key_name='FERNET_KEY'): 43 | try: 44 | key = load_key(key_name) 45 | except: 46 | key = os.environ.get(key_name) 47 | f = Fernet(key) 48 | with open(filename + "-encrypted", "rb") as file: 49 | encrypted_data = file.read() 50 | decrypted_data = f.decrypt(encrypted_data) 51 | return decrypted_data 52 | 53 | -------------------------------------------------------------------------------- /src/static/images/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/static/csv/team.csv: -------------------------------------------------------------------------------- 1 | ,code,draw,form,id,loss,name,played,points,position,short_name,strength,team_division,unavailable,win,strength_overall_home,strength_overall_away,strength_attack_home,strength_attack_away,strength_defence_home,strength_defence_away,pulse_id 2 | 0,3,0,,1,0,Arsenal,0,0,0,ARS,4,,False,0,1240,1250,1160,1210,1190,1230,1 3 | 1,7,0,,2,0,Aston Villa,0,0,0,AVL,3,,False,0,1130,1150,1140,1170,1120,1160,2 4 | 2,36,0,,3,0,Brighton,0,0,0,BHA,3,,False,0,1100,1120,1110,1130,1100,1120,131 5 | 3,90,0,,4,0,Burnley,0,0,0,BUR,3,,False,0,1060,1100,1130,1150,1010,1020,43 6 | 4,8,0,,5,0,Chelsea,0,0,0,CHE,4,,False,0,1250,1280,1180,1250,1230,1260,4 7 | 5,31,0,,6,0,Crystal Palace,0,0,0,CRY,3,,False,0,1080,1120,1100,1130,1020,1040,6 8 | 6,11,0,,7,0,Everton,0,0,0,EVE,4,,False,0,1200,1210,1160,1220,1210,1240,7 9 | 7,54,0,,8,0,Fulham,0,0,0,FUL,2,,False,0,1000,1020,1020,1020,1000,1010,34 10 | 8,13,0,,9,0,Leicester,0,0,0,LEI,4,,False,0,1200,1240,1190,1230,1200,1230,26 11 | 9,2,0,,10,0,Leeds,0,0,0,LEE,3,,False,0,1090,1130,1110,1140,1100,1120,9 12 | 10,14,0,,11,0,Liverpool,0,0,0,LIV,5,,False,0,1330,1350,1250,1310,1320,1340,10 13 | 11,43,0,,12,0,Man City,0,0,0,MCI,5,,False,0,1330,1350,1250,1310,1330,1340,11 14 | 12,1,0,,13,0,Man Utd,0,0,0,MUN,4,,False,0,1220,1250,1210,1230,1200,1290,12 15 | 13,4,0,,14,0,Newcastle,0,0,0,NEW,3,,False,0,1090,1130,1100,1130,1030,1070,23 16 | 14,49,0,,15,0,Sheffield Utd,0,0,0,SHU,3,,False,0,1140,1160,1130,1160,1020,1050,18 17 | 15,20,0,,16,0,Southampton,0,0,0,SOU,3,,False,0,1090,1120,1130,1130,1110,1120,20 18 | 16,6,0,,17,0,Spurs,0,0,0,TOT,4,,False,0,1270,1290,1160,1180,1260,1290,21 19 | 17,35,0,,18,0,West Brom,0,0,0,WBA,2,,False,0,1020,1040,1010,1010,990,1020,36 20 | 18,21,0,,19,0,West Ham,0,0,0,WHU,3,,False,0,1140,1170,1150,1170,1160,1200,25 21 | 19,39,0,,20,0,Wolves,0,0,0,WOL,4,,False,0,1190,1210,1190,1230,1140,1190,38 22 | -------------------------------------------------------------------------------- /src/templates/signup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The Super Secret FPL Tool Newsletter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | https://fpl-optimized.beehiiv.com/subscribe 39 |
40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /.github/workflows/sample_fpl.yml: -------------------------------------------------------------------------------- 1 | name: Sample-FPL 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | GW: 7 | description: 'GW Number' 8 | required: true 9 | 10 | jobs: 11 | build-and-run: 12 | name: Sample values from FPL 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Check current directory 17 | run: pwd 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | - name: Checkout webpage branch 21 | uses: actions/checkout@v2 22 | with: 23 | ref: 'webpage' 24 | path: ./src/build 25 | - name: Make sure data folder exists 26 | run: mkdir -p ./src/build/sample 27 | - name: Pull docker image 28 | run: docker pull sertalpbilal/selenium_docker:latest 29 | - name: Run docker image 30 | run: | 31 | if [ ${{ github.event.inputs.GW }} == 'season' ]; then 32 | docker run -t --rm -e LANG=C.UTF-8 -v $(pwd):/app sertalpbilal/selenium_docker bash -c "cd /app/scripts && python3 -m pip install -r requirements.txt && cd /app/src && python3 -c \"import collect; collect.sample_all_season()\" " 33 | else 34 | docker run -t --rm -e LANG=C.UTF-8 -e GW=${{ github.event.inputs.GW }} -v $(pwd):/app sertalpbilal/selenium_docker bash -c "cd /app/scripts && python3 -m pip install -r requirements.txt && chmod +x *.sh && ./sample.sh" 35 | fi 36 | shell: bash 37 | - name: Add changes to the branch 38 | run: | 39 | cd src/build 40 | git add -u 41 | git add . 42 | git config user.name "Github Action" 43 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 44 | git commit -m "Automated build - Data Sample GW${{ github.event.inputs.GW }} $GITHUB_RUN_ID" 45 | git push 46 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Populate-Website 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: engine 7 | schedule: 8 | - cron: "0 4,10,16,22 * * *" 9 | 10 | jobs: 11 | build-and-run: 12 | name: Run the automated Docker build 13 | runs-on: ubuntu-latest 14 | if: "!contains(github.event.commits[0].message, '[skip ci]')" 15 | 16 | steps: 17 | - name: Check current directory 18 | run: pwd 19 | - name: Checkout repository 20 | uses: actions/checkout@v2 21 | - name: Checkout webpage branch 22 | uses: actions/checkout@v2 23 | with: 24 | ref: "webpage" 25 | path: ./build 26 | - name: Make sure data folder exists 27 | run: mkdir -p ./build/data 28 | - name: Build docker image 29 | run: docker build -t webapp . 30 | - name: Run docker image with optimization 31 | if: "!contains(github.event.commits[0].message, '[skip opt]')" 32 | run: | 33 | docker run -t --rm -e LANG=C.UTF-8 -e FERNET_KEY=$FERNET_KEY -v $(pwd)/build:/app/build webapp bash -c "python3 -c \"import encrypt; encrypt.decrypt('simulator.py')\" && python3 run.py" 34 | env: 35 | FERNET_KEY: ${{ secrets.FERNET_KEY }} 36 | REVIEW_KEY: ${{ secrets.REVIEW_KEY }} 37 | PATREON_COOKIE_REVIEW: ${{ secrets.PATREON_COOKIE_REVIEW }} 38 | - name: Only populate pages, no optimization 39 | if: "contains(github.event.commits[0].message, '[skip opt]')" 40 | run: | 41 | docker run -t --rm -e LANG=C.UTF-8 -e REVIEW_KEY=$REVIEW_KEY -v $(pwd)/build:/app/build webapp bash -c "python3 run.py skip-opt" 42 | env: 43 | REVIEW_KEY: ${{ secrets.REVIEW_KEY }} 44 | - name: Add changes to the branch 45 | run: | 46 | cd build 47 | git add -u 48 | git add . 49 | git config user.name "Github Action" 50 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 51 | git commit -m "Automated build $GITHUB_RUN_ID" 52 | git push 53 | -------------------------------------------------------------------------------- /src/analytics_league/history.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import itertools 4 | import json 5 | import time 6 | import random 7 | 8 | from analytics_league.utils import read_league_teams, get_current_gw, save_to_json 9 | from collect import get_fpl_info, FPL_API 10 | from concurrent.futures import ProcessPoolExecutor 11 | from urllib.request import urlopen 12 | 13 | 14 | def get_team_picks(team, gw): 15 | return get_fpl_info('picks', PID=team, GW=gw) 16 | 17 | 18 | def get_team_history(team, start, end): 19 | if start is None: 20 | start = 1 21 | gw_range = range(start, end+1) 22 | with ProcessPoolExecutor(max_workers=8) as executor: 23 | picks = list(executor.map(get_team_picks, itertools.repeat(team), gw_range)) 24 | 25 | transfers = get_fpl_info('transfers', PID=team) 26 | history = get_fpl_info('history', PID=team) 27 | return {'picks': picks, 'transfers': transfers, 'history': history} 28 | 29 | 30 | def get_analytics_league_history(): 31 | ''' 32 | Returns all squad picks, transfer, and chip history for Analytics League teams 33 | ''' 34 | teams = read_league_teams() 35 | last_gw = get_current_gw() 36 | 37 | for team in teams: 38 | print(team) 39 | team.update(get_team_history(team=team['id'], start=None, end=last_gw)) 40 | 41 | return teams 42 | 43 | 44 | 45 | def get_only_team_history(team): 46 | history = get_fpl_info('history', PID=team) 47 | return {'id': team, 'history': history} 48 | 49 | 50 | def get_rank_n_player(rank): 51 | page = ((rank-1)//50)+1 52 | order = (rank-1) % 50 53 | print(f"Fetching rank {rank}") 54 | try: 55 | with urlopen(FPL_API['overall'].format(LID=314, P=page)) as url: 56 | page_data = json.loads(url.read().decode()) 57 | tid = page_data['standings']['results'][order]['entry'] 58 | print(f"Done {rank}") 59 | return get_only_team_history(tid) 60 | except: 61 | print("Encountered page access error, waiting 5 seconds") 62 | time.sleep(5) 63 | print(f"Error {rank}") 64 | return None 65 | 66 | 67 | def sample_within_range(): 68 | 69 | target = 1000000 70 | nsample = 5000 71 | 72 | player_targets = random.sample(range(1, target+1), nsample) 73 | with ProcessPoolExecutor(max_workers=16) as executor: 74 | results = list(executor.map(get_rank_n_player, player_targets)) 75 | fetched_history = [i for i in results if i is not None] 76 | return fetched_history 77 | 78 | 79 | if __name__ == '__main__': 80 | # h = get_analytics_league_history() 81 | # save_to_json('history.json', h) 82 | 83 | h = sample_within_range() 84 | save_to_json('history.json', h) 85 | -------------------------------------------------------------------------------- /src/prep.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # Data prep steps 4 | 5 | import pandas as pd 6 | import glob 7 | import pathlib 8 | 9 | from app import folder_order 10 | 11 | def get_GW_folder(gw=None): 12 | 13 | if gw is None: 14 | all_dates = glob.glob(f'build/data/*/*/*/input/') 15 | else: 16 | all_dates = glob.glob(f'build/data/*/GW{gw}/*/input/') 17 | all_dates.sort(key=folder_order, reverse=True) 18 | return all_dates[0] 19 | 20 | 21 | def get_multistage_data(gw=None, n=3): 22 | """ Returns the multi-period data available right before given GW """ 23 | 24 | input_folder = pathlib.Path(get_GW_folder(gw)) 25 | 26 | df = pd.read_csv(input_folder / 'element_gameweek.csv') 27 | next_week = df['event'].min() 28 | element_gameweek_df = df[df['event'] < next_week+n].copy() 29 | 30 | sum_md_df = element_gameweek_df.groupby(['player_id', 'web_name'])['points_md'].sum() 31 | sum_md_df.sort_values(inplace=True, ascending=False) 32 | sum_md_df = sum_md_df.reset_index().set_index('player_id').copy() 33 | 34 | df = pd.read_csv(input_folder / 'element.csv') 35 | element_df = df.copy().set_index('id') 36 | element_df['ict_sum'] = element_df['influence'] + element_df['creativity'] + element_df['threat'] 37 | element_gameweek_df = pd.merge(left=element_gameweek_df, right=df, how='inner', left_on=['player_id'], right_on=['id'], suffixes=('', '_extra')) 38 | element_gameweek_df['rawxp'] = element_gameweek_df.apply(lambda r: r['points_md'] * 90 / max(1, r['xmins_md']), axis=1) 39 | elements = element_gameweek_df['player_id'].unique().tolist() 40 | gameweeks = element_gameweek_df['event'].unique().tolist() 41 | total_weeks = len(gameweeks) 42 | element_gameweek_df.set_index(['player_id', 'event'], inplace=True, drop=True) 43 | popular_element_df = element_df.sort_values(by=['selected_by_percent'], ascending=False)[['web_name', 'selected_by_percent']] 44 | 45 | team_codes = element_gameweek_df['team_code'].unique().tolist() 46 | 47 | types_df = pd.read_csv(input_folder / 'element_type.csv') 48 | types = types_df['id'].to_list() 49 | types_df.set_index('id', inplace=True, drop=True) 50 | 51 | position = [1,2,3,4] 52 | element_gameweek = [(e, g) for e in elements for g in gameweeks] 53 | 54 | return { 55 | 'gw': gw, 56 | 'input_folder': input_folder, 57 | 'element_gameweek_df': element_gameweek_df, 58 | 'sum_md_df': sum_md_df, 59 | 'element_df': element_df, 60 | 'total_weeks': total_weeks, 61 | 'team_codes': team_codes, 62 | 'types': types, 63 | 'types_df': types_df, 64 | 'position': position, 65 | 'element_gameweek': element_gameweek, 66 | 'elements': elements, 67 | 'gameweeks': gameweeks 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/regenerate_element_gameweek.yml: -------------------------------------------------------------------------------- 1 | name: Regenerate Element Gameweek Data 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | gw: 7 | description: "Gameweek number to regenerate (leave empty to process all available projections)" 8 | required: false 9 | type: string 10 | force_new: 11 | description: "Force creation of new folder even if one exists" 12 | required: false 13 | type: boolean 14 | default: false 15 | 16 | jobs: 17 | regenerate: 18 | name: Regenerate element_gameweek.csv files 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Checkout engine branch 23 | uses: actions/checkout@v2 24 | with: 25 | ref: engine 26 | 27 | - name: Checkout webpage branch 28 | uses: actions/checkout@v2 29 | with: 30 | ref: webpage 31 | path: ./build 32 | 33 | - name: Set up Python 34 | uses: actions/setup-python@v2 35 | with: 36 | python-version: "3.9" 37 | 38 | - name: Install dependencies 39 | run: | 40 | python -m pip install --upgrade pip 41 | pip install pandas numpy pytz 42 | 43 | - name: Run regeneration script (all GWs) 44 | if: ${{ github.event.inputs.gw == '' && !github.event.inputs.force_new }} 45 | run: | 46 | cd src 47 | python regenerate_element_gameweek.py 48 | 49 | - name: Run regeneration script (specific GW) 50 | if: ${{ github.event.inputs.gw != '' && !github.event.inputs.force_new }} 51 | run: | 52 | cd src 53 | python regenerate_element_gameweek.py --gw ${{ github.event.inputs.gw }} 54 | 55 | - name: Run regeneration script (all GWs, force new) 56 | if: ${{ github.event.inputs.gw == '' && github.event.inputs.force_new }} 57 | run: | 58 | cd src 59 | python regenerate_element_gameweek.py --force-new 60 | 61 | - name: Run regeneration script (specific GW, force new) 62 | if: ${{ github.event.inputs.gw != '' && github.event.inputs.force_new }} 63 | run: | 64 | cd src 65 | python regenerate_element_gameweek.py --gw ${{ github.event.inputs.gw }} --force-new 66 | 67 | - name: Commit and push changes 68 | run: | 69 | cd build 70 | git config user.name "Github Action" 71 | git config user.email "41898282+github-actions[bot]@users.noreply.github.com" 72 | git add -A 73 | if git diff --staged --quiet; then 74 | echo "No changes to commit" 75 | else 76 | git commit -m "Regenerate element_gameweek data (GW: ${{ github.event.inputs.gw || 'all' }}) [skip ci]" 77 | git push 78 | echo "Changes pushed successfully" 79 | fi 80 | -------------------------------------------------------------------------------- /src/templates/ownership_trend.html: -------------------------------------------------------------------------------- 1 | "% include 'header.html' %" 2 | 3 | 4 |
5 | 6 |
7 |
8 |
9 |
Ownership Trends
10 | 11 |
12 | 13 |
14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
FPL APISample - Top 1K
NameTeamNowLast GWTrendLast GWEstimatedPosPriceID
{{ i.web_name }}{{ team_codes[i.team_code].short }}{{ i.selected_by_percent }}{{ last_gw_data[i.id] }}{{ getWithSign(parseFloat(i.selected_by_percent) - parseFloat(last_gw_data[i.id])) }}{{ parseFloat(sample_last_gw[i.id]).toFixed(2) }}{{ (sample_estimated[i.id]).toFixed(2) }}{{ element_type[i.element_type].short }}{{ parseInt(i.now_cost)/10 }}{{ i.id }}
53 |
54 | Loading data... Please wait... 55 | 56 |
57 |
58 |
59 |
60 |
61 | 62 | 63 |
64 | 65 | "% with scripts=["main", "ownership_trend"] %" "% include 'footer.html' %" "% endwith %" -------------------------------------------------------------------------------- /src/templates/history.html: -------------------------------------------------------------------------------- 1 | "% include 'header.html' %" 2 | 3 |
4 |
5 |
6 |
7 |
FPL Point History
8 |
9 |
10 | Choose a season 11 | 12 | 16 | 17 |
18 |
19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 45 | 46 | 47 |
IDPosNameCVTotal 31 | {{ gw }} 32 |
{{ player[0] }}{{ pos_dict[player_dict[player[0]].element_type] }}{{ player_dict[player[0]].web_name }}{{ _.round(player_dict[player[0]].now_cost / 10,1) }}{{ player[1] }} 43 | {{ _.get(player_gw_dict, `${player[0]}.${gw}.total`, "") }} 44 |
48 |
49 |
50 |
51 |
52 |
53 | 54 |
55 |
56 |
57 | 60 |
61 |
62 |
63 | 64 |
65 | 66 | 69 | 70 | "% with scripts=["main", "history"] %" "% include 'footer.html' %" "% endwith %" -------------------------------------------------------------------------------- /src/static/js/history.js: -------------------------------------------------------------------------------- 1 | 2 | var app = new Vue({ 3 | el: '#app', 4 | data: { 5 | seasons: seasons, 6 | selected_season: '', 7 | season_data: undefined, 8 | player_gw_dict: undefined, 9 | player_sums: undefined, 10 | players_sorted: undefined, 11 | table_ready: false, 12 | pos_dict: {1: 'G', 2: 'D', 3: 'M', 4: 'F'} 13 | }, 14 | computed: { 15 | player_dict() { 16 | if (!this.season_data) { return undefined } 17 | let elements = this.season_data.elements 18 | return _.fromPairs(elements.map(i => [i.id, i])) 19 | } 20 | }, 21 | methods: { 22 | update_season() { 23 | 24 | let pts_table = $("#pts_table") 25 | if (pts_table) { 26 | pts_table.DataTable().destroy(); 27 | } 28 | this.table_ready = false; 29 | 30 | let s = this.selected_season 31 | if (s == '') { 32 | this.season_data = undefined 33 | this.player_gw_dict = undefined 34 | this.player_sums = undefined 35 | this.players_sorted = undefined 36 | return 37 | } 38 | 39 | read_local_file(`data/${s}/points.json`).then((points_data) => { 40 | read_local_file(`data/${s}/static.json`).then((static_data) => { 41 | let final_dict = {} 42 | _.each(points_data, (entries, gw) => { // (val, key) 43 | _.forEach(entries, entry => { 44 | let player = { 45 | 'id': entry.id, 46 | 'total': _.sum(entry.e.map(i => i.stats.map(j => j.points)).flat()) 47 | } 48 | _.set(final_dict, `${entry.id}.${gw}`, player) 49 | }) 50 | }) 51 | 52 | this.player_gw_dict = Object.freeze(final_dict) 53 | this.player_sums = Object.freeze(_(final_dict).mapValues((val,key) => _.sumBy(val, 'total')).value()) 54 | this.players_sorted = Object.freeze(_(this.player_sums).toPairs().orderBy([1], ['desc']).value()) 55 | 56 | this.season_data = Object.freeze(static_data) 57 | 58 | this.$nextTick(() => { 59 | let table = $("#pts_table").DataTable({ 60 | "order": [4], 61 | // "lengthChange": true, 62 | // // "pageLength": 100, 63 | "searching": true, 64 | // "info": false, 65 | "paging": true, 66 | "columnDefs": [], 67 | scrollX: true, 68 | buttons: [ 69 | 'copy', 'csv' 70 | ] 71 | }); 72 | table.cells("td").invalidate().draw(); 73 | table.buttons().container() 74 | .appendTo('#buttons'); 75 | 76 | this.table_ready = true; 77 | }) 78 | }) 79 | }) 80 | }, 81 | pts_color(pid, gw) { 82 | let pts = _.get(this.player_gw_dict, `${pid}.${gw}.total`, undefined) 83 | if (pts == undefined) { return "none" } 84 | let colors = d3.scaleLinear().domain([-100, -1, 0, 3, 15]).range(["#e19797", "#e19797", "#ffffff", "#ffffff", "#7FD3D9"]) 85 | return colors(pts) 86 | } 87 | } 88 | }) 89 | 90 | $(document).ready(() => { 91 | 92 | }) 93 | -------------------------------------------------------------------------------- /src/static/json/fpl_analytics.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "id": 2221044, "twitter": "sertalpbilal" }, 3 | { "id": 367, "twitter": "FplSimpson" }, 4 | { "id": 17973, "twitter": "FplStatsdan" }, 5 | { "id": 44142, "twitter": "FF_Trout" }, 6 | { "id": 2227, "twitter": "dilksybab" }, 7 | { "id": 36828, "twitter": "theFPLkiwi" }, 8 | { "id": 4473, "twitter": "jameso_12" }, 9 | { "id": 159049, "twitter": "api_kakayou" }, 10 | { "id": 2134733, "twitter": "usafpl" }, 11 | { "id": 11891, "twitter": "CazzaFPL" }, 12 | { "id": 115, "twitter": "PlanetosFPL" }, 13 | { "id": 59177, "twitter": "analytic_fpl" }, 14 | { "id": 262, "twitter": "wee_rogue" }, 15 | { "id": 15673, "twitter": "mtthwmn" }, 16 | { "id": 1339675, "twitter": "cmusson32" }, 17 | { "id": 106304, "twitter": "FPLstudent2" }, 18 | { "id": 10350, "twitter": "peterhandleson" }, 19 | { "id": 293113, "twitter": "chrishp" }, 20 | { "id": 2243390, "twitter": "plfantasy" }, 21 | { "id": 741, "twitter": "FPLmeta" }, 22 | { "id": 23491, "twitter": "FplAuto" }, 23 | { "id": 227516, "twitter": "NourEldin_z3r0" }, 24 | { "id": 23337, "twitter": "UkendtPerson45" }, 25 | { "id": 3561, "twitter": "MikkelTokvam" }, 26 | { "id": 210810, "twitter": "Klaudioz" }, 27 | { "id": 204827, "twitter": "FantasianPL" }, 28 | { "id": 10187, "twitter": "rama96ab" }, 29 | { "id": 634300, "twitter": "FPL_LFE" }, 30 | { "id": 17157, "twitter": "_jtreble" }, 31 | { "id": 1341730, "twitter": "DanielSaetre" }, 32 | { "id": 274376, "twitter": "FBinary01" }, 33 | { "id": 3861, "twitter": "FPL_Data" }, 34 | { "id": 2978, "twitter": "FPL_ElStatto" }, 35 | { "id": 795, "twitter": "FplSigurd" }, 36 | { "id": 726810, "twitter": "JoeBopara" }, 37 | { "id": 4033, "twitter": "dLincolnlawyer" }, 38 | { "id": 1188, "twitter": "FPL_Statoholic" }, 39 | { "id": 184956, "twitter": "FplAmateur" }, 40 | { "id": 4084, "twitter": "wolacola" }, 41 | { "id": 16804, "twitter": "Hashtag_MEGA" }, 42 | { "id": 2409, "twitter": "dav_fifa" }, 43 | { "id": 393944, "twitter": "MaulanaRiandi" }, 44 | { "id": 392517, "twitter": "Rak464" }, 45 | { "id": 1310, "twitter": "ArneBarsnes" }, 46 | { "id": 416185, "twitter": "FPL_PapaSmurf" }, 47 | { "id": 33480, "twitter": "gezza96" }, 48 | { "id": 7193, "twitter": "Raghy78d" }, 49 | { "id": 26787, "twitter": "pdhFPL" }, 50 | { "id": 5390907, "twitter": "NeilRankinZA" }, 51 | { "id": 50155, "twitter": "EuanThompson" }, 52 | { "id": 501, "twitter": "FPLrookie1" }, 53 | { "id": 1280279, "twitter": "FplBudget" }, 54 | { "id": 150618, "twitter": "FF_Vader" }, 55 | { "id": 2977, "twitter": "alasdairtweets" }, 56 | { "id": 53926, "twitter": "fpl_analytic" }, 57 | { "id": 2072032, "twitter": "stephendeyoung" }, 58 | { "id": 496, "twitter": "James_Ingram15" }, 59 | { "id": 896, "twitter": "FPL_Kid" }, 60 | { "id": 3137, "twitter": "fpl_jan" }, 61 | { "id": 1480, "twitter": "FPL_Mezzala" }, 62 | { "id": 69496, "twitter": "morstats" }, 63 | { "id": 374292, "twitter": "FplOpinion" }, 64 | { "id": 472, "twitter": "_NeilFPL" }, 65 | { "id": 121535, "twitter": "UnderstandingF8" }, 66 | { "id": 92246, "twitter": "EejRoberts" }, 67 | { "id": 1531, "twitter": "FPL_Gents" }, 68 | { "id": 4417, "twitter": "FPLHaggis" }, 69 | { "id": 12322, "twitter": "Jumpthewave" }, 70 | { "id": 3683, "twitter": "DarraghNoonan" }, 71 | { "id": 1697, "twitter": "FPL_Chase" }, 72 | { "id": 300408, "twitter": "jonwalker88" }, 73 | { "id": 241725, "twitter": "TomDavies_17" }, 74 | { "id": 32982, "twitter": "FodWeTrust" }, 75 | { "id": 123767, "twitter": "FplPass" }, 76 | { "id": 455, "twitter": "prparsons" }, 77 | { "id": 1215, "twitter": "FplJeb" }, 78 | { "id": 267, "twitter": "JosesBusDrivers" }, 79 | { "id": 5063, "twitter": "hidethecactus" }, 80 | { "id": 884492, "twitter": "tweetsofagooner" }, 81 | { "id": 5365714, "twitter": "FplRafiki" }, 82 | { "id": 43745, "twitter": "FPL_Audit" }, 83 | { "id": 48534, "twitter": "DaveCZfpl" }, 84 | { "id": 10744, "twitter": "PaulGiurculet" }, 85 | { "id": 127899, "twitter": "k_rybaczuk" }, 86 | { "id": 421075, "twitter": "CoalportGlen" }, 87 | { "id": 22274, "twitter": "FPLvariance" }, 88 | { "id": 9754, "twitter": "SchadenfreudeFF" }, 89 | { "id": 3468, "twitter": "desphorin" }, 90 | { "id": 372678, "twitter": "arnarrafn1" }, 91 | { "id": 3966, "twitter": "FPL_SOS" } 92 | ] 93 | -------------------------------------------------------------------------------- /src/static/js/top_squads.js: -------------------------------------------------------------------------------- 1 | var app = new Vue({ 2 | el: '#app', 3 | data: { 4 | season: season, 5 | gw: gw, 6 | date: date, 7 | listdates: listdates, 8 | solutions: [] 9 | }, 10 | methods: { 11 | refresh_results() { 12 | season = this.season; 13 | gw = this.gw; 14 | date = this.date; 15 | load_all(); 16 | }, 17 | close_date() { 18 | $("#dateModal").modal('hide'); 19 | }, 20 | setSolutions(values) { 21 | this.solutions = _.cloneDeep(values); 22 | } 23 | }, 24 | computed: { 25 | seasongwdate: { 26 | get: function() { 27 | return this.season + " / " + this.gw + " / " + this.date; 28 | }, 29 | set: function(value) { 30 | let v = value.split(' / '); 31 | this.season = v[0]; 32 | this.gw = v[1]; 33 | this.date = v[2]; 34 | this.refresh_results(); 35 | } 36 | }, 37 | parsed_solutions: function() { 38 | let solutions = this.solutions; 39 | solutions.forEach( 40 | function(i) { 41 | i.AO = (i.ownership.squad.reduce((sume, el) => sume + el, 0) / 15).toFixed(2); 42 | i.SO = (i.ownership.sum).toFixed(2); 43 | let weeks = Object.keys(i.lineup); 44 | let week_returns = Object.fromEntries(weeks.map(w => [w, Object.fromEntries(i.squad.map((j, k) => [j, i.xP[w][k]]))])); 45 | 46 | 47 | let lineup_points = Object.fromEntries(weeks.map(w => [w, i.lineup[w].map(p => week_returns[w][p])])); 48 | let pts_lineup = Object.values(lineup_points).map(w => w.reduce((a, b) => a + b, 0)).reduce((a, b) => a + b, 0); 49 | let pts_captain = Object.entries(i.captain).map(v => week_returns[v[0]][v[1]]).reduce((a, b) => a + b, 0); 50 | i.LxP = (pts_lineup + pts_captain).toFixed(2); 51 | 52 | let bench_points = Object.fromEntries(weeks.map(w => [w, Object.entries(i.bench[w]).map(k => week_returns[w][k[1]])])) 53 | let weighted_bench_pts = Object.values(bench_points).map(v => v.map((p, x) => 10 ** Math.min(-x, -1) * p)); 54 | let pts_bench = Object.values(weighted_bench_pts).map(v => v.reduce((a, b) => a + b, 0)).reduce((a, b) => a + b, 0); 55 | i.WxP = (pts_lineup + pts_captain + pts_bench).toFixed(2); 56 | 57 | let pts_bb = Object.values(bench_points).map(v => v.reduce((a, b) => a + b, 0)).reduce((a, b) => a + b, 0); 58 | i.BBxP = (pts_lineup + pts_captain + pts_bb).toFixed(2); 59 | 60 | let squad_with_type = _.zip(i.squad, i.element_type) 61 | i.sorted_squad = squad_with_type.sort(function(a, b) { 62 | if (a[1] == b[1]) { return a[0] - b[0]; } 63 | return a[1] - b[1]; 64 | }) 65 | i.player_names = _.zipObject(i.squad, i.players); 66 | 67 | }) 68 | return solutions; 69 | } 70 | } 71 | }) 72 | 73 | function load_all() { 74 | $.ajax({ 75 | type: "GET", 76 | url: `data/${season}/${gw}/${date}/output/iterative_model.json`, 77 | dataType: "json", 78 | success: function(data) { 79 | app.setSolutions(data); 80 | $(document).ready(function() { 81 | $('#top_squads_table').DataTable({ 82 | searchBuilder: {}, 83 | dom: 'Qfrtip', 84 | responsive: { 85 | details: { 86 | display: $.fn.dataTable.Responsive.display.modal({ 87 | header: function(row) { 88 | var data = row.data(); 89 | return 'Details for ' + data[0] + ' ' + data[1]; 90 | } 91 | }), 92 | renderer: $.fn.dataTable.Responsive.renderer.tableAll({ 93 | tableClass: 'table' 94 | }) 95 | } 96 | } 97 | }); 98 | }); 99 | }, 100 | error: function(xhr, status, error) { 101 | // app.setSolution(name, []); 102 | console.log(error); 103 | console.error(xhr, status, error); 104 | } 105 | }); 106 | } 107 | 108 | $(document).ready(function() { 109 | load_all(); 110 | }); -------------------------------------------------------------------------------- /src/templates/footer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | "% if no_ev != True %" 4 |
5 |
6 | 10 | 14 |
15 |
16 | "% endif %" 17 | 18 | 19 | 22 | 25 | 27 | 28 | 33 | 36 | 39 | 40 | 41 | 43 | 45 | 46 | 47 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 63 | 64 | 65 | 66 | 81 | 82 | "% for item in scripts %" 83 | 84 | "% endfor %" 85 | 86 | 89 | 90 | 91 | 92 | 93 |
Last update: ** last_update **
94 | 95 | -------------------------------------------------------------------------------- /src/static/js/load.js: -------------------------------------------------------------------------------- 1 | var app = new Vue({ 2 | el: '#app', 3 | data: { 4 | raw_data: [], 5 | last_loaded: [] 6 | }, 7 | computed: { 8 | is_ready() { 9 | return !_.isEmpty(this.local_data) 10 | }, 11 | parsed_data() { 12 | let d = this.raw_data 13 | return d.map(i => {return {...i, parsed: jQuery.csv.toObjects(i.data)}}) 14 | }, 15 | reverse_parsed_data() { 16 | return _.reverse(this.parsed_data) 17 | }, 18 | sorted_local_data() { 19 | return [] 20 | } 21 | }, 22 | methods: { 23 | saveLocal() { 24 | window.localStorage.setItem('xp_storage', JSON.stringify(this.raw_data)) 25 | }, 26 | addData(raw_data, parsed_data, meta_data) { 27 | // disable same GW old ones 28 | this.raw_data.filter(i => i.meta.start_gw == meta_data.start_gw).forEach((e) => { 29 | e.meta.status = 'ready' 30 | }) 31 | 32 | meta_data.status = 'active' 33 | this.raw_data.push({meta: meta_data, data: raw_data}) 34 | 35 | this.saveLocal() 36 | }, 37 | activateData(id) { 38 | let this_data = this.raw_data.find(i => i.meta.id == id) 39 | let other_active = this.raw_data.filter(i => (i.meta.start_gw == this_data.meta.start_gw) && (i.meta.status == 'active')) 40 | other_active.forEach((o) => { 41 | o.meta.status = 'ready' 42 | }) 43 | 44 | this_data.meta.status = 'active' 45 | 46 | this.saveLocal() 47 | }, 48 | deleteData(id) { 49 | let this_data = this.raw_data.find(i => i.meta.id == id) 50 | if(this_data) { 51 | let gw = this_data.meta.start_gw 52 | this.raw_data = this.raw_data.filter(i => i.meta.id !== id) 53 | let new_active = this.raw_data.find(i => i.meta.start_gw == gw) 54 | if (new_active) { 55 | new_active.meta.status = 'active' 56 | } 57 | } 58 | 59 | this.saveLocal() 60 | }, 61 | addNameToData() { 62 | if (_.isEmpty(this.last_loaded)) { return } 63 | let targets = this.raw_data.filter(i => this.last_loaded.includes(i.meta.id)) 64 | let new_name = $("#data-name-entry").val() 65 | if (!_.isEmpty(new_name)) { 66 | targets.forEach((e) => { 67 | e.meta.filename = new_name 68 | }) 69 | } 70 | 71 | this.saveLocal() 72 | }, 73 | renameData(id) { 74 | this.last_loaded = [id] 75 | $("#name-modal").modal('show') 76 | } 77 | } 78 | }); 79 | 80 | function getRandomId() { 81 | return Math.random().toString(36).replace('0.', '') 82 | } 83 | 84 | function handleFiles(files) { 85 | app.last_loaded = []; 86 | ([...files]).forEach((f) => { 87 | let reader = new FileReader(); 88 | reader.addEventListener('load', function (e) { 89 | let csvdata = e.target.result; 90 | let obj_data = jQuery.csv.toObjects(csvdata); 91 | 92 | // check 93 | let valid = true 94 | let meta_data = {} 95 | let keys = Object.keys(obj_data[0]) 96 | 97 | // other types? 98 | if (keys.filter(i => i.indexOf("Pts ") != -1).length > 0) { // kiwi type data 99 | let gw_keys = keys.filter(i => i.indexOf("Pts ") != -1 && i.indexOf('-') == -1) 100 | // convert kiwi type to review type 101 | gw_keys.forEach((g) => { 102 | let gw = g.split(' ')[1] 103 | csvdata = csvdata.replace('xPts ' + gw, gw + "_Pts") 104 | csvdata = csvdata.replace('xMin ' + gw, gw + "_xMins") 105 | }) 106 | obj_data = jQuery.csv.toObjects(csvdata); 107 | } 108 | 109 | // Albert data 110 | const regex = /X(\d{1,2}\_)/gm; 111 | const subst = `$1`; 112 | csvdata = csvdata.replace(regex, subst); 113 | csvdata = csvdata.replace(/"id"/gm, '"ID"') 114 | obj_data = jQuery.csv.toObjects(csvdata); 115 | 116 | keys = Object.keys(obj_data[0]) 117 | if (keys.filter(i => i.indexOf("_Pts") != -1).length > 0) { // fplreview type data 118 | let gw_keys = keys.filter(i => i.indexOf("_Pts") != -1) 119 | let gws = gw_keys.map(i => parseInt(i.split("_")[0])) 120 | let start_gw = _.min(gws) 121 | let finish_gw = _.max(gws) 122 | let horizon = parseInt(finish_gw) - parseInt(start_gw) + 1 123 | let filename = f.name 124 | let dt = JSON.parse(JSON.stringify(new Date())) 125 | let status = 'ready' 126 | let id = getRandomId() 127 | meta_data = {id, gws, start_gw, finish_gw, horizon, filename, dt, status} 128 | } 129 | else { 130 | valid = false 131 | } 132 | 133 | // push 134 | if(valid) { 135 | app.addData(csvdata, obj_data, meta_data) 136 | app.last_loaded.push(meta_data.id) 137 | } 138 | else { 139 | // show error message here 140 | } 141 | }); 142 | reader.readAsText(f, 'ISO-8859-1'); 143 | }) 144 | $("#name-modal").modal('show') 145 | } 146 | 147 | $(document).ready(() => { 148 | // define drag-drop events 149 | let dropArea = document.getElementById('load_area'); 150 | 151 | ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { 152 | dropArea.addEventListener(eventName, preventDefaults, false) 153 | }); 154 | 155 | function preventDefaults(e) { 156 | e.preventDefault() 157 | e.stopPropagation() 158 | }; 159 | 160 | ['dragenter', 'dragover'].forEach(eventName => { 161 | dropArea.addEventListener(eventName, highlight, false) 162 | }); 163 | 164 | ['dragleave', 'drop'].forEach(eventName => { 165 | dropArea.addEventListener(eventName, unhighlight, false) 166 | }); 167 | 168 | function highlight(e) { 169 | dropArea.classList.add('highlight_box') 170 | }; 171 | 172 | function unhighlight(e) { 173 | dropArea.classList.remove('highlight_box') 174 | }; 175 | 176 | dropArea.addEventListener('drop', handleDrop, false) 177 | 178 | function handleDrop(e) { 179 | let dt = e.dataTransfer 180 | let files = dt.files 181 | handleFiles(files) 182 | } 183 | 184 | // get existing data 185 | let raw_data = window.localStorage.getItem('xp_storage') 186 | if (raw_data) { 187 | let json_data = JSON.parse(raw_data) 188 | app.raw_data = json_data 189 | } 190 | else { 191 | // nothing -_- 192 | } 193 | 194 | 195 | 196 | }) -------------------------------------------------------------------------------- /archive/highlights-fdr-parts.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
Fixture Difficulty Analysis
5 |
6 | Fixture difficulty ratings of your picks through season using FiveThirtyEight data 7 |
8 |
9 |
10 |
11 | Defense Difficulty 12 |
13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 |
{{ team.short }}
19 | {{ rounded(team.offense_ratio*100,0) }}% 20 |
23 |
24 |
25 |
26 | Offense Difficulty 27 |
28 | 29 | 30 | 31 | 32 | 33 | 36 | 37 |
{{ team.short }}
34 | {{ rounded(team.defense_ratio*100,0) }}% 35 |
38 |
39 |
40 |
41 |
42 |
43 | Fixture Difficulty per Player Pick 44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
GWNamePosTeamOppDiff. RatioPoints
{{ p.gw }}{{ fpl_element[p.id].web_name }}{{ p.position }}{{ p.self_team.short }}{{ p.opp_team.short }}{{ rounded(p.player_fdr_ratio*100,1) }}%{{ p.total }}
83 |
84 |
85 |
86 | 87 |
88 |
89 |
90 | Distribution of Selection per FDR Tier 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 110 |
TierGKDFMDFWTotal
{{ (g-.25)*100 }}-{{ g*100 }}%{{ user_fdr_tiers.tiers[p][i] }}{{ getSum([1,2,3,4].map(j => user_fdr_tiers.tiers[j][i])) }}
111 |
112 |
113 | 114 |
115 |
116 |
117 |
-------------------------------------------------------------------------------- /src/static/images/field.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 64 | 75 | 87 | 99 | 111 | 118 | 125 | 136 | 148 | 156 | 164 | 170 | 175 | 176 | 177 | -------------------------------------------------------------------------------- /src/static/js/who_played.js: -------------------------------------------------------------------------------- 1 | 2 | let scaled_color = ({ 3 | d, 4 | min = 0, 5 | max = 1, 6 | colors = ["#70f4ff", "#fbfbfb", "#ff8763"], //["#c6311b", "#676767", "#3390f7"] 7 | revert = false 8 | }) => { 9 | let dom = [-100, 0, 0.5, 1, 100] 10 | if (revert) { 11 | dom = [100, 1, 0.5, 0, -100] 12 | } 13 | let v = d3.scaleLinear().domain(dom).range([colors[0], colors[0], colors[1], colors[2], colors[2]]) 14 | return v((d - min) / (max - min)) 15 | } 16 | 17 | var app = new Vue({ 18 | el: '#app', 19 | data: { 20 | season: season, 21 | next_gw: gw, 22 | // 23 | points_data: undefined, 24 | // 25 | loading: false, 26 | ready: false, 27 | players: [], 28 | teams: teams_ordered, 29 | gameweeks: gameweeks, 30 | elements: elements, 31 | // selection 32 | team_selected: undefined, 33 | gw_selected: undefined, 34 | pos_selected: "", 35 | scaled_color: scaled_color, 36 | show_pts: false 37 | }, 38 | computed: { 39 | filtered_players() { 40 | if (this.team_selected == undefined || this.gw_selected == undefined) { return []} 41 | if (!(this.gw_selected in this.points_data)) { return []} 42 | let filtered = this.points_data[this.gw_selected] 43 | let player_ids = this.elements.filter(i => (this.team_selected == "all" || i.team == this.team_selected) && (this.pos_selected == "" || parseInt(this.pos_selected) == i.element_type)).map(i => i.id) 44 | filtered = filtered.filter(i => player_ids.includes(i.id)) 45 | filtered = _.orderBy(filtered, ['player.element_type', 'total_min', 'total_pts'], ['asc', 'desc', 'desc']) 46 | let lineup_count = _.countBy(filtered, 'player.element_type') 47 | let ctr = {1: 1, 2: 1, 3: 1, 4: 1} 48 | filtered.forEach((p) => { 49 | let pos = p.player.element_type 50 | let cnt = ctr[pos] 51 | ctr[pos] += 1 52 | let pos_tot = lineup_count[pos] 53 | p.x = 122 / (pos_tot + 1) * cnt - 17; 54 | p.y = (pos - 1) * 35 + 3; 55 | }) 56 | return filtered 57 | }, 58 | filtered_by_season_players() { 59 | if (this.team_selected == undefined) { return []} 60 | let players = this.elements.filter(i => (this.team_selected == "all" || i.team == this.team_selected) && (this.pos_selected == "" || parseInt(this.pos_selected) == i.element_type)) 61 | let overall_total = 0 62 | players.forEach((p) => { 63 | p.min_data = {} 64 | p.pts_data = {} 65 | p.total_min = 0 66 | p.total_pts = 0 67 | p.matches_played = 0 68 | for (let w of this.gameweeks) { 69 | let entry = w in this.points_data ? this.points_data[w].find(i => i.id == p.id) : undefined 70 | if (entry == undefined) { 71 | p.min_data[w] = 0 72 | p.pts_data[w] = 0 73 | } 74 | else { 75 | p.matches_played += 1 76 | p.min_data[w] = entry.total_min 77 | p.total_min += entry.total_min 78 | if (w == this.next_gw) { 79 | overall_total += entry.total_min 80 | } 81 | p.pts_data[w] = entry.total_pts 82 | p.total_pts += entry.total_pts 83 | } 84 | 85 | } 86 | p.muted = p.total_min == 0 87 | }) 88 | players.forEach((p) => { 89 | p.overall_total = overall_total 90 | p.min_per_game = p.total_min / (p.matches_played > 0 ? p.matches_played : 1) 91 | p.pts_per_game = p.total_pts / (p.matches_played > 0 ? p.matches_played : 1) 92 | }) 93 | players = _.orderBy(players, ['element_type', 'total_min', 'id'], ['asc', 'desc', 'asc']) 94 | return players 95 | }, 96 | team_played() { 97 | if (this.team_selected == undefined || _.isEmpty(this.filtered_by_season_players)) { return true} 98 | return this.filtered_by_season_players[0].overall_total > 0 99 | }, 100 | team_selected_name() { 101 | if (this.team_selected == undefined) { return ""} 102 | if (this.team_selected == "all") { return "All" } 103 | return this.teams[this.team_selected-1].name 104 | }, 105 | team_computed: { 106 | get: () => { 107 | return app.team_selected 108 | }, 109 | set: (v) => { 110 | app.remove_dt() 111 | app.team_selected = v 112 | app.load_dt() 113 | } 114 | }, 115 | pos_computed: { 116 | get: () => { 117 | return app.pos_selected 118 | }, 119 | set: (v) => { 120 | app.remove_dt() 121 | app.pos_selected = v 122 | app.load_dt() 123 | } 124 | } 125 | }, 126 | methods: { 127 | remove_dt() { 128 | $("#value_table").DataTable().destroy(); 129 | }, 130 | load_dt() { 131 | this.$nextTick(() => { 132 | let table = $("#value_table").DataTable({ 133 | "order": [], 134 | "lengthChange": false, 135 | "pageLength": 100, 136 | "searching": false, 137 | "info": false, 138 | "paging": false, 139 | "columnDefs": [] 140 | }); 141 | table.cells("td").invalidate().draw(); 142 | }) 143 | }, 144 | invalidate_cache() { 145 | this.$nextTick(() => { 146 | var table = $("#value_table").DataTable(); 147 | table.cells("td").invalidate().draw(); 148 | }) 149 | }, 150 | togglePt(e) { 151 | this.remove_dt() 152 | this.show_pts = e.currentTarget.checked 153 | this.load_dt() 154 | } 155 | } 156 | }) 157 | 158 | 159 | async function get_points() { 160 | return $.ajax({ 161 | type: "GET", 162 | url: `data/${season}/points.json`, 163 | async: true, 164 | dataType: "json", 165 | success: (data) => { 166 | Object.values(data).forEach((w) => w.forEach((p) => { 167 | p.player = elements.find(i => i.id == p.id) 168 | p.minutes = p.e.map(i => i.stats?.find(j => j.identifier == 'minutes')?.value ?? 90) 169 | p.total_min = getSum(p.minutes) 170 | p.full_time = p.total_min == p.e.length * 90 171 | p.total_pts = getSum(p.e.map(i => i.stats.map(j => j.points)).flat()) 172 | })) 173 | app.points_data = data; 174 | }, 175 | error: (xhr, status, error) => { 176 | console.log(error); 177 | console.error(xhr, status, error); 178 | } 179 | }); 180 | } 181 | 182 | $(document).ready(() => { 183 | Promise.all([ 184 | get_points() 185 | ]).then((values) => { 186 | app.ready = true 187 | app.load_dt() 188 | }) 189 | .catch((error) => { 190 | console.error("An error has occured: " + error); 191 | }); 192 | }) 193 | -------------------------------------------------------------------------------- /src/templates/spirit_team.html: -------------------------------------------------------------------------------- 1 | "% include 'header.html' %" 2 | 3 | 4 | 5 |
6 | 7 |
8 |
9 |
10 |
Spirit Team
11 |
12 | Playing FPL and wondering which team resembles your performance the most? 13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 | 21 |
22 |
23 |
24 | 25 |
26 |
27 |
28 |
Your Spirit Team: {{ spirit_team[0].club }}
29 | 30 |
Match Ratio (R-Squared): {{ (spirit_team[0].r.final_r2*100).toFixed(0) }}%
31 |
{{ spirit_team[0].r.string }}
32 |
33 |
34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 | 42 |
43 |
44 |
45 |
46 |
47 |
48 | 49 | 50 | User Rank 51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | 60 | 61 | Team Position 62 |
63 |
64 |
65 |
66 |
67 |
68 | 69 |
70 |
71 | 97 |
98 |
99 | 100 |
101 |
102 | All Matches 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 |
TeamMatch (R2)
{{ s.club }}{{ (s.r.final_r2*100).toFixed(0) }}%
113 |
114 |
115 | 116 |
117 |
118 |
119 |
120 | 121 |
122 | 123 | 124 | 125 | "% with scripts=["main", "spirit_team"] %" "% include 'footer.html' %" "% endwith %" -------------------------------------------------------------------------------- /src/static/js/weekly.js: -------------------------------------------------------------------------------- 1 | var app = new Vue({ 2 | el: '#app', 3 | data: { 4 | season: season, 5 | gw: gw, 6 | date: date, 7 | listdates: listdates, 8 | solutions: { 9 | 'no_limit_best_11': [], 10 | 'limited_best_15': [], 11 | 'limited_best_15_weighted': [], 12 | 'limited_best_15_bb': [], 13 | 'limited_best_differential': [], 14 | 'limited_best_set_and_forget': [] 15 | } 16 | }, 17 | methods: { 18 | setSolution: function(key, vals) { 19 | this.solutions[key] = _.cloneDeep(vals); 20 | }, 21 | get_field_solution(name) { 22 | let pos_ctr = { 1: 1, 2: 1, 3: 1, 4: 1, 'B': 1 } 23 | sol = this.solutions[name] 24 | sol.forEach( 25 | function(i) { 26 | let cnt = sol.filter(j => j.element_type == i.element_type).filter(j => !j.starting_lineup || j.starting_lineup == "1").length; 27 | i.xP = parseFloat(i.points_md); 28 | if (!i.starting_lineup || i.starting_lineup == "1") { 29 | i.x = 122 / (cnt + 1) * pos_ctr[parseInt(i.element_type)] - 17; 30 | i.y = (parseInt(i.element_type) - 1) * 35 + 3; 31 | pos_ctr[parseInt(i.element_type)] += 1; 32 | } else { 33 | i.x = 122 / 5 * pos_ctr['B'] - 17; 34 | pos_ctr['B'] += 1; 35 | i.y = 138.5; 36 | } 37 | i.is_captain = (i.is_captain == "True"); 38 | i.team_name = team_codes[parseInt(i.team_code)]; 39 | i.now_cost_str = (parseFloat(i.now_cost) / 10).toFixed(1); 40 | i.selected_by_percent = parseFloat(i.selected_by_percent) || -1; 41 | }) 42 | return sol; 43 | }, 44 | get_solution_with_details(name) { 45 | let data = this.get_field_solution(name); 46 | let cost = data.map(i => i.now_cost).reduce((a, b) => parseFloat(a) + parseFloat(b), 0); 47 | let lineup_xp = data.filter(j => !j.starting_lineup || j.starting_lineup == "1").map(i => i.xP * parseInt(i.multiplier)).reduce((a, b) => a + b, 0); 48 | let bench_xp = data.filter(j => j.starting_lineup == "0").map(i => i.xP).reduce((a, b) => a + b, 0); 49 | let weighted_xp = lineup_xp + 0.1 * bench_xp; 50 | let bb_xp = lineup_xp + 1 * bench_xp; 51 | let avg_own = data.map(i => i.selected_by_percent).reduce((a, b) => a + b, 0); 52 | if (avg_own <= 0) { avg_own = "-" } else { avg_own = (avg_own / data.length).toFixed(2) + "%"; } 53 | return { data: data, cost: (cost / 10).toFixed(1), lineup_xp: lineup_xp.toFixed(2), weighted_xp: weighted_xp.toFixed(2), bb_xp: bb_xp.toFixed(2), avg_own: avg_own }; 54 | }, 55 | refresh_results() { 56 | season = this.season; 57 | gw = this.gw; 58 | date = this.date; 59 | load_all(); 60 | }, 61 | close_date() { 62 | $("#dateModal").modal('hide'); 63 | } 64 | }, 65 | computed: { 66 | field_solution_1: function() { 67 | return this.get_solution_with_details("no_limit_best_11"); 68 | }, 69 | field_solution_2: function() { 70 | return this.get_solution_with_details("limited_best_15"); 71 | }, 72 | field_solution_3: function() { 73 | return this.get_solution_with_details("limited_best_15_weighted"); 74 | }, 75 | field_solution_4: function() { 76 | return this.get_solution_with_details("limited_best_15_bb"); 77 | }, 78 | field_solution_5: function() { 79 | return this.get_solution_with_details("limited_best_differential"); 80 | }, 81 | field_solution_6: function() { 82 | return this.get_solution_with_details("limited_best_set_and_forget"); 83 | }, 84 | seasongwdate: { 85 | get: function() { 86 | return this.season + " / " + this.gw + " / " + this.date; 87 | }, 88 | set: function(value) { 89 | let v = value.split(' / '); 90 | this.season = v[0]; 91 | this.gw = v[1]; 92 | this.date = v[2]; 93 | this.refresh_results(); 94 | } 95 | } 96 | } 97 | }) 98 | 99 | function load_solution_from_file(name) { 100 | $.ajax({ 101 | type: "GET", 102 | url: `data/${season}/${gw}/${date}/output/${name}.csv`, 103 | dataType: "text", 104 | success: function(data) { 105 | tablevals = data.split('\n').map(i => i.split(',')); 106 | keys = tablevals[0]; 107 | values = tablevals.slice(1); 108 | values_filtered = values.filter(i => i.length > 1); 109 | let squad = values_filtered.map(i => _.zipObject(keys, i)); 110 | app.setSolution(name, squad); 111 | }, 112 | error: function(xhr, status, error) { 113 | app.setSolution(name, []); 114 | } 115 | }); 116 | } 117 | 118 | function load_all() { 119 | load_solution_from_file("no_limit_best_11"); 120 | load_solution_from_file("limited_best_15"); 121 | load_solution_from_file("limited_best_15_weighted"); 122 | load_solution_from_file("limited_best_15_bb"); 123 | load_solution_from_file("limited_best_differential"); 124 | load_solution_from_file("limited_best_set_and_forget"); 125 | } 126 | 127 | load_all(); 128 | 129 | // $.ajax({ 130 | // type: "GET", 131 | // url: `data/${season}/${gw}/${date}/output/no_limit_best_11.csv`, 132 | // dataType: "text", 133 | // success: function(data) { 134 | // tablevals = data.split('\n').map(i => i.split(',')); 135 | // keys = tablevals[0]; 136 | // values = tablevals.slice(1); 137 | // values_filtered = values.filter(i => i.length > 1); 138 | // let squad = values_filtered.map(i => _.zipObject(keys, i)); 139 | // app.setSolution('unlimited_best_11', squad); 140 | // } 141 | // }); 142 | 143 | 144 | // $('#unlimited_best_11_canvas').load('static/images/field.svg', function() { 145 | // $('#unlimited_best_11_canvas').find('svg').addClass('field mx-auto d-block'); 146 | // let x = SVG("#unlimited_best_11_canvas > svg"); 147 | // // let d = x.bbox(); 148 | // $.ajax({ 149 | // type: "GET", 150 | // url: `data/${season}/${gw}/${date}/output/no_limit_best_11.csv`, 151 | // dataType: "text", 152 | // success: function(data) { 153 | // tablevals = data.split('\n').map(i => i.split(',')); 154 | // keys = tablevals[0]; 155 | // values = tablevals.slice(1); 156 | // let squad = values.map(i => _.zipObject(keys, i)); 157 | // app.setSolution('unlimited_best_11', squad); 158 | // } 159 | // }); 160 | // }); 161 | // } 162 | // }); 163 | 164 | 165 | // field_svg = ''; 166 | // jersey_svg = ''; 167 | 168 | // function set_field_svg(v) { 169 | // field_svg = v; 170 | // } 171 | 172 | // function set_jersey_svg(v) { 173 | // jersey_svg = v; 174 | // } 175 | 176 | // $.ajax({ 177 | // type: "GET", 178 | // url: `static/images/jersey.svg`, 179 | // async: false, 180 | // dataType: "text", 181 | // success: function(jsvg) { 182 | // set_jersey_svg(jsvg); 183 | // } 184 | // }); 185 | 186 | // $.ajax({ 187 | // type: "GET", 188 | // url: `static/images/field.svg`, 189 | // async: false, 190 | // dataType: "text", 191 | // success: function(fsvg) { 192 | // set_field_svg(fsvg); 193 | // } 194 | // }); -------------------------------------------------------------------------------- /src/templates/top_squads.html: -------------------------------------------------------------------------------- 1 | "% include 'header.html' %" 2 | 3 |
4 |
5 |
6 |
7 |
8 |
9 | Snapshot 10 | 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
Top 50 Squads
24 |
An interactive list of top 50 optimal squads for next 3 gameweeks with performance indicators
25 |
26 |
27 |
28 | 29 | 30 | 31 | 32 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 52 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
IDSquadStatsKPI
CostAO%SO%LxPWxPBBxP
{{ sol.id+1 }} 53 |
54 | 55 |
56 | {{ sol.player_names[p[0]] }} 57 |
58 |
59 |
{{ sol.total_cost / 10 }}{{ sol.AO }}{{ sol.SO }}{{ sol.LxP }}{{ sol.WxP }}{{ sol.BBxP }}
69 |
70 |
71 |
72 |
73 |
74 |
75 | 76 |
77 |
78 |
79 |
80 |
Q/A
81 | What are these squads? 82 |

I use mixed-integer linear programming to find optimal squads to analyze and compare. I use an iterative approach: I find the best possible solution ("the" optimal) and cut off that particular solution until I obtain 50 different solutions.

83 | How do these problems are solved? 84 |

All instances are solved to optimality using mathmetical modeling. Data is provided by FPL Review, problems are modeled using sasoptpy package and finally solved using 85 | open-source CBC solver. You can see the modeling source code 86 | here. Feel free to clone the repository and play with the model.

87 | What is the purpose? 88 |

The main purpose is to show how optimization can be used in sports, specifically fantasy sports in this case. I hope these squads can give you new ideas for upcoming weeks.

89 |

You can find another squad with exactly the same objective value, but cannot get anything strictly better. Optimization models are solved to exact optimality.

90 | How often / when do you update this page? 91 |

This page is automatically built everyday using GitHub Actions at 10:00 GMT (5 AM EST) and 17:00 GMT (12 PM EST) and whenever there is a code change.

92 | I have a suggestion or improvement to your model. How can I contribute? 93 |

Feel free to submit a pull request on GitHub, or open an issue.

94 |
95 | 98 |
99 |
100 |
101 | 102 |
103 | 104 | 123 | 124 | "% with scripts=["main", "top_squads"] %" "% include 'footer.html' %" "% endwith %" -------------------------------------------------------------------------------- /src/static/js/ownership_trend.js: -------------------------------------------------------------------------------- 1 | var app = new Vue({ 2 | el: '#app', 3 | data: { 4 | is_ready: false, 5 | season: season, 6 | gw: gw, 7 | next_gw: next_gw, 8 | date: date, 9 | listdates: listdates, 10 | element_now: {}, 11 | element_history: [], 12 | last_gw_all: {}, 13 | sample_data: {}, 14 | estimation: {}, 15 | table: "" 16 | }, 17 | methods: { 18 | save_gw_data(data, name, season, gw, date) { 19 | if (name == "now") { 20 | this.element_now = { 'season': season, 'gw': gw, 'date': date, 'values': data }; 21 | } 22 | this.element_history.push({ 'season': season, 'gw': gw, 'date': date, 'values': data }); 23 | }, 24 | saveSampleDataAndInit(success, data) { 25 | if (success) { 26 | this.sample_data = data; 27 | } 28 | this.prepareLastGW(); 29 | this.prepareEstimation(); 30 | this.is_ready = true; 31 | this.$nextTick(() => { 32 | this.table = $("#trend_table").DataTable({ 33 | "order": [2], 34 | info: false, 35 | scrollX: true, 36 | paging: false, 37 | scrollY: "400px", 38 | scrollCollapse: true, 39 | fixedColumns: true, 40 | lengthChange: false, 41 | "processing": true, 42 | buttons: [ 43 | 'copy', 'csv' 44 | ], 45 | //dom: 'Bfrtip' 46 | }); 47 | this.table.buttons().container() 48 | .appendTo('.col-md-6:eq(0)'); 49 | // new $.fn.dataTable.FixedHeader(this.table); 50 | draw_ownership_plot(); 51 | $("#loading_box").remove(); 52 | 53 | }) 54 | }, 55 | prepareLastGW() { 56 | let vals = this.sample_data[1000].filter(i => i.team !== undefined); 57 | let players = _.cloneDeep(this.element_now.values); 58 | 59 | let all_players = vals.map(i => i.data.picks).flat().filter(i => i.multiplier > 0).map(i => i.element); 60 | players.forEach(function(val, idx) { 61 | let cnt = all_players.filter(i => i.toString() == val.id).length; 62 | let new_ownership = (cnt + 0.0) / vals.length * 100; 63 | val.selected_by_percent = new_ownership + 0; 64 | }); 65 | players = Object.fromEntries(players.map(i => [i.id, i.selected_by_percent])); 66 | this.last_gw_all = players; 67 | }, 68 | prepareEstimation() { 69 | let players = _.cloneDeep(this.element_now.values); 70 | let cg = this.current_gw_data; 71 | let lg = this.last_gw_data; 72 | let slw = this.sample_last_gw; 73 | 74 | players.forEach(function(val) { 75 | val.selected_by_percent = Math.min(Math.max(slw[val.id] + (cg[val.id] - lg[val.id]), 0), 100); 76 | }); 77 | this.estimation = Object.fromEntries(players.map(i => [i.id, i.selected_by_percent])); 78 | } 79 | }, 80 | computed: { 81 | // is_ready() { 82 | // if (this.element_history.length < 7) { return false; } 83 | // if (Object.keys(this.sample_data).length == 0) { return false; } 84 | // return true; 85 | // }, 86 | is_fully_ready() { 87 | if (!this.is_ready) { return false; } 88 | if (Object.keys(this.estimation) == 0) { return false; } 89 | return true; 90 | }, 91 | current_gw_data() { 92 | if (Object.keys(this.element_now).length == 0) { return {} }; 93 | return Object.fromEntries(this.element_now.values.map(i => [i.id, i.selected_by_percent])); 94 | }, 95 | last_gw_data() { 96 | if (Object.keys(this.last_gw_all).length == 0) { return []; } 97 | if (this.element_history.length < 7) { return []; } 98 | let last_gw = "GW" + (parseInt(this.gw.slice(2)) - 1); 99 | let vals = this.element_history.filter(i => i.gw == last_gw)[0].values; 100 | vals = Object.fromEntries(vals.map(i => [i.id, i.selected_by_percent])); 101 | let defvals = Object.fromEntries(this.element_now.values.map(i => [i.id, 0])) 102 | return {...defvals, ...vals }; 103 | }, 104 | sample_last_gw() { 105 | return this.last_gw_all; 106 | }, 107 | sample_estimated() { 108 | if (this.is_fully_ready) { 109 | return this.estimation; 110 | } else { 111 | return {}; 112 | } 113 | } 114 | } 115 | }) 116 | 117 | 118 | function get_element_data(name, season, gw, date) { 119 | $.ajax({ 120 | type: "GET", 121 | url: `data/${season}/${gw}/${date}/input/element.csv`, 122 | dataType: "text", 123 | success: function(data) { 124 | tablevals = data.split('\n').map(i => i.split(',')); 125 | keys = tablevals[0]; 126 | values = tablevals.slice(1); 127 | let el_data = values.map(i => _.zipObject(keys, i)); 128 | el_data = el_data.filter(i => i.id != undefined); 129 | app.save_gw_data(el_data, name, season, gw, date); 130 | }, 131 | error: function(xhr, status, error) { 132 | console.log(error); 133 | console.error(xhr, status, error); 134 | } 135 | }); 136 | } 137 | 138 | function draw_ownership_plot() { 139 | 140 | return; 141 | 142 | if (!app.is_ready) { return; } 143 | 144 | var margin = { top: 10, right: 100, bottom: 30, left: 30 }, 145 | width = 300 - margin.left - margin.right, 146 | height = 200 - margin.top - margin.bottom; 147 | 148 | var svg = d3.select("#ownership_graph").append("svg") 149 | .attr("viewBox", `0 0 ${(width + margin.left + margin.right)} ${(height + margin.top + margin.bottom)}`) 150 | .attr("class", "mx-auto d-block") 151 | .append("g") 152 | .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 153 | 154 | let data = app.element_history; 155 | data = data.map(i => Object.fromEntries(i.values.map(j => [j.id, j.selected_by_percent]))) 156 | let keys = Object.keys(data[0]); 157 | let graph_data = []; 158 | for (let k of keys) { 159 | let vals = data.map(i => i[k]).filter(i => i != undefined); 160 | if (vals.length == 7) { 161 | let change = vals[0] - vals[6]; 162 | let avg = (vals.reduce((a, b) => parseFloat(a) + parseFloat(b), 0)) / vals.length; 163 | graph_data.push({ 'id': k, 'values': vals, 'change': change, 'avg': avg }); 164 | } 165 | } 166 | graph_data = graph_data.filter(i => i.avg > 8).filter(i => Math.abs(i.change) > 5); 167 | 168 | var x = d3.scaleLinear() 169 | .domain([0, 6]) 170 | .range([0, width]); 171 | svg.append("g") 172 | .attr("transform", "translate(0," + height + ")") 173 | .call(d3.axisBottom(x)); 174 | 175 | var y = d3.scaleLinear() 176 | .domain([0, 70]) 177 | .range([height, 0]); 178 | svg.append("g") 179 | .call(d3.axisLeft(y)); 180 | 181 | var line = d3.line() 182 | .x(function(d, i) { return x(i) }) 183 | .y(function(d, i) { return y(d) }) 184 | svg.selectAll("myLines") 185 | .data(graph_data) 186 | .enter() 187 | .append("path") 188 | .attr("d", function(d) { return line(d.values) }) 189 | // .attr("stroke", function(d) { return myColor(d.name) }) 190 | .attr("stroke", "white") 191 | .style("stroke-width", 1) 192 | .style("fill", "none") 193 | 194 | // USE DROPDOWN 195 | 196 | } 197 | 198 | 199 | $(document).ready(function() { 200 | get_element_data("now", season, gw, date); 201 | for (let i of listdates.slice(1, 11)) { 202 | let point = i.split('/').map(i => i.trim()); 203 | get_element_data('historic', point[0], point[1], point[2]); 204 | } 205 | 206 | let last_gw = (parseInt(gw.slice(2)) - 1); 207 | 208 | $.ajax({ 209 | type: "GET", 210 | url: `sample/${last_gw}/fpl_sampled.json`, 211 | dataType: "json", 212 | success: function(data) { 213 | app.saveSampleDataAndInit(true, data); 214 | } 215 | }); 216 | }); -------------------------------------------------------------------------------- /src/templates/load.html: -------------------------------------------------------------------------------- 1 | "% include 'header.html' %" 2 | 3 |
4 |
5 |
6 |
7 |
8 |
Data Load Beta
9 | 10 |
11 |
12 | Load your own data to be used in other pages 13 |
14 |
15 |
If you are subscribed to an FPL Projection source1, or have your own data2, you can load them here and use on other pages.
16 |
The data is stored on your browser.
17 |
18 |

Drag and Drop Your Files Here

19 |
20 | 21 | 22 |
23 |
24 |
25 |
Data Status
26 |
27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 53 | 54 | 55 |
Start GWHorizonNameDatetimeStatusAction
{{ i.meta.start_gw }}{{ i.meta.horizon }}{{ i.meta.filename }}{{ i.meta.dt }}{{ i.meta.status }} 45 | 46 | 47 | 49 | 50 | 51 | 52 |
56 |
57 |
58 |
59 |
60 |
1Currently works with FPLReview Massive Data (Premium), and theFPLKiwi.
61 |
2You can give the data in FPLReview format: Either sorted player list by ID, or with `ID` column and `GW_Pts` format (e.g. 34_Pts and 34_xMins)
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | 70 | 71 | 91 | 92 | 93 | 94 | 135 | 136 | 137 | "% with scripts=["main", "load"] %" "% include 'footer.html' %" "% endwith %" -------------------------------------------------------------------------------- /src/static/js/manager_form.js: -------------------------------------------------------------------------------- 1 | var app = new Vue({ 2 | el: '#app', 3 | data: { 4 | this_gw: gw, 5 | is_active: is_active, 6 | fpl_id: undefined, 7 | fpl_history: undefined, 8 | fpl_average: undefined, 9 | range: 3, 10 | base: 0.45, 11 | gifstyle: "spongebob" 12 | }, 13 | computed: { 14 | is_ready() { 15 | return this.fpl_id !== undefined && this.fpl_history !== undefined 16 | }, 17 | manager_ratings() { 18 | if (!this.is_ready) { return undefined; } 19 | const range = this.range 20 | const keys = this.fpl_history.current.map(i => i.event).slice(-range) 21 | const manager_values = Object.fromEntries(this.fpl_history.current.map(i => [i.event, i.points])) 22 | const average_values = this.fpl_average 23 | const ratios = [] 24 | const mults = [] 25 | const last_gw = Math.max(...keys) 26 | let score = 0; 27 | let divider = 0; 28 | keys.forEach(week => { 29 | let multiplier = Math.pow(this.base, last_gw - week) 30 | let manager_ratio = Math.min(1, manager_values[week] / (2*average_values[week])) 31 | score = score + multiplier * manager_ratio 32 | divider = divider + multiplier 33 | mults.push(multiplier) 34 | ratios.push(manager_ratio) 35 | }) 36 | const forecast = score/divider; 37 | return {ratios, forecast, keys, manager_values, average_values, mults}; 38 | }, 39 | final_tier() { 40 | return Math.floor(this.manager_ratings.forecast * 5) + 1 41 | }, 42 | tier_text() { 43 | let text = { 44 | 1: "Terrible Form", 45 | 2: "Unlucky Form", 46 | 3: "Casual Form", 47 | 4: "Good Form", 48 | 5: "Great Form - Ready to attack GW!" 49 | } 50 | return text[this.final_tier] 51 | } 52 | }, 53 | methods: { 54 | enterTeam() { 55 | let el = document.querySelector("#teamID_input") 56 | if (el.value.length == 0) { return } 57 | this.fpl_history = undefined 58 | this.fpl_id = parseInt(el.value) 59 | el.value = "" 60 | fetch_fpl_history() 61 | }, 62 | submitTeam(e) { 63 | if (e.keyCode === 13) { 64 | this.enterTeam() 65 | } 66 | }, 67 | openParamModal() { 68 | $("#paramModal").modal('show') 69 | }, 70 | refresh_graph() { 71 | draw_ratings() 72 | } 73 | } 74 | }); 75 | 76 | async function fetch_fpl_history() { 77 | return get_team_history(app.fpl_id).then((data) => { 78 | app.fpl_history = data; 79 | app.$nextTick(() => { 80 | draw_ratings() 81 | }) 82 | }).catch((e) => { 83 | console.log("Error", e) 84 | }) 85 | } 86 | 87 | async function fetch_fpl_averages() { 88 | return get_fpl_main_data().then((data) => { 89 | app.fpl_average = Object.fromEntries(data.events.filter(i => i.average_entry_score > 0).map(i => [i.id, i.average_entry_score])) 90 | }).catch((e) => { 91 | console.log("Error", e) 92 | }) 93 | } 94 | 95 | 96 | 97 | async function draw_ratings() { 98 | 99 | let c = document.querySelector("#canvas") 100 | c.innerHTML = "" 101 | 102 | var margin = { top: 5, right: 35, bottom: 20, left: 35 }, 103 | width = 250 - margin.left - margin.right, 104 | height = 180 - margin.top - margin.bottom; 105 | 106 | let cnv = d3.select("#canvas") 107 | .append("svg") 108 | .attr("id", "canvas-graph") 109 | .attr("viewBox", `0 0 ${(width + margin.left + margin.right)} ${(height + margin.top + margin.bottom)}`) 110 | .attr('class', 'pull-center') 111 | .style('display', 'block') 112 | .style('padding-bottom', '10px'); 113 | 114 | let svg = cnv.append('g').attr('class', 'svg-actual').attr('transform', 'translate(43,4)'); 115 | let grayrect = svg.append('g'); 116 | grayrect.append('rect').attr('fill', '#5a5d5c').attr('width', width).attr('height', height); 117 | 118 | data = app.manager_ratings 119 | 120 | let x_low = Math.min(...data.keys)-0.5 121 | let x_high = Math.max(...data.keys)+1+0.5 122 | 123 | let y_high = 1 124 | let y_low = 0 125 | 126 | // Axis-x 127 | var x = d3.scaleLinear().domain([x_low, x_high]).range([0, width]); 128 | svg.append('g') 129 | .call( 130 | d3.axisBottom(x).ticks(data.keys.length + 1) 131 | .tickSize(height) 132 | ); 133 | 134 | var y = d3.scaleLinear().domain([y_low, y_high]).range([height, 0]); 135 | svg.append('g').attr("transform", "translate(" + width + ",0)").call(d3.axisLeft(y).tickSize(width).tickFormat((d) => d*100 + '%')); 136 | 137 | svg.call(g => g.selectAll(".tick text") 138 | .attr("fill", "white")) 139 | .call(g => g.selectAll(".tick line") 140 | .attr("stroke-dasharray", "4,2") 141 | .attr("stroke", "#f1f1f1") 142 | .attr("stroke-width", 0.5) 143 | .attr("opacity", 0.1) 144 | .style('pointer-events', 'none')) 145 | .call(g => g.selectAll(".domain") 146 | .attr("opacity", 0)); 147 | 148 | // Title - x 149 | svg.append("text") 150 | .attr("text-anchor", "middle") 151 | .attr("x", width / 2) 152 | .attr("y", height + 17) 153 | .attr("font-size", "4pt") 154 | .attr("fill", "white") 155 | .text("GW"); 156 | 157 | // Title - y1 158 | svg.append("text") 159 | .attr("text-anchor", "middle") 160 | .attr("transform", "rotate(-90)") 161 | .attr("x", -height / 2) 162 | .attr("y", -20) 163 | .attr("font-size", "5pt") 164 | .attr("fill", "white") 165 | .text("Performance"); 166 | 167 | svg.call(s => s.selectAll(".tick").attr("font-size", "4pt")); 168 | 169 | svg.append('path') 170 | .datum(data.ratios) 171 | .attr("fill", "none") 172 | .attr("stroke", "#3BB9E2") 173 | .attr("stroke-opacity", 1) 174 | .attr("stroke-width", 1) 175 | .style('pointer-events', 'none') 176 | .attr("d", d3.line() 177 | .x((d,i) => x(data.keys[i])) 178 | .y((d) => y(d)) 179 | ); 180 | 181 | const forecast_rates = data.ratios.slice(-1).concat(data.forecast) 182 | const next_gw = parseInt(data.keys.slice(-1))+1 183 | const forecast_gw = data.keys.slice(-1).concat(next_gw) 184 | 185 | svg.append('path') 186 | .datum(forecast_rates) 187 | .attr("fill", "none") 188 | .attr("stroke", "#A6CE51") 189 | .attr("stroke-opacity", 0.8) 190 | .style("stroke-dasharray", "5,1") 191 | .attr("stroke-width", 1) 192 | .style('pointer-events', 'none') 193 | .attr("d", d3.line() 194 | .x((d,i) => x(forecast_gw[i])) 195 | .y((d) => y(d)) 196 | ); 197 | 198 | // scatter 199 | svg.selectAll() 200 | .data(data.ratios) 201 | .enter() 202 | .append('circle') 203 | .attr('r', 3) 204 | .attr('cx', (d,i) => x(data.keys[i])) 205 | .attr('cy', (d) => y(d)) 206 | .attr('fill', '#3BB9E2') 207 | 208 | svg.selectAll() 209 | .data([data.forecast]) 210 | .enter() 211 | .append('circle') 212 | .attr('r', 3) 213 | .attr('cx', (d) => x(next_gw)) 214 | .attr('cy', (d) => y(d)) 215 | .attr('fill', '#A6CE51') 216 | .attr('opacity', 1) 217 | 218 | 219 | // svg.selectAll() 220 | // .data(data.ratios) 221 | // .enter() 222 | // .append('rect') 223 | // .attr('width', 20) 224 | // .attr('height', 10) 225 | // .attr('x', (d,i) => x(data.keys[i])-10) 226 | // .attr('y', (d) => y(d) + 5) 227 | // .attr('fill', 'black') 228 | // .attr('opacity', 0.3) 229 | 230 | // svg.selectAll() 231 | // .data(data.ratios) 232 | // .enter() 233 | // .append('text') 234 | // .attr("text-anchor", "middle") 235 | // .attr("alignment-baseline", "middle") 236 | // .attr('x', (d,i) => x(data.keys[i])) 237 | // .attr('y', (d) => y(d) + 10) 238 | // .attr("font-size", "4pt") 239 | // .attr("fill", "white") 240 | // .text((d, i) => d); 241 | 242 | 243 | } 244 | 245 | 246 | 247 | $(document).ready(() => { 248 | Promise.all([ 249 | fetch_fpl_averages() 250 | ]).then((values) => { 251 | app.$nextTick(() => { 252 | console.log('READY!') 253 | }) 254 | }) 255 | .catch((error) => { 256 | console.error("An error has occured: " + error); 257 | }); 258 | }) -------------------------------------------------------------------------------- /src/templates/manager_form.html: -------------------------------------------------------------------------------- 1 | "% include 'header.html' %" 2 | 3 |
4 | 5 |
6 |
7 |
8 |
FPL Manager Form
9 |
10 | Measuring your recent performance to find out your managerial form! 11 |
12 |
13 | 14 |
15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 |
Your Manager Form
30 |
{{ rounded(manager_ratings.forecast * 100, 0) + '%' }}
31 |
Tier: {{ tier_text }}
32 | 33 |
34 | 35 | 42 |
43 |
44 |
45 |
46 |
47 | 48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
GWPointsAverageRatingWeight
{{ w }}{{ manager_ratings.manager_values[w] }}{{ manager_ratings.average_values[w] }}{{ rounded(manager_ratings.ratios[i] * 100, 1) }}%{{ rounded(manager_ratings.mults[i] * 100, 1) }}%
70 |
71 | 72 |
73 | 74 |
75 |
76 |
77 |
78 |
79 | Fetching data... 80 | 81 |
82 |
83 | 84 |
85 |
86 |
87 |
88 | 89 | 90 | 95 | 96 |
97 |
98 |
99 |
100 |
Q/A
101 | What does manager form mean? 102 |

Just a metric for fun, it does not really mean anything.

103 | How do you calculate it? 104 |

I get your FPL points of recent weeks and compare it to the overall FPL average in a range of 0% and 100%. Then, I apply Exponential Moving Average on top of it to show your final form. Full calculation is as follows:

105 |

$$\text{form} = \frac{\displaystyle \sum_{g=0}^{n} p_{g} \cdot b^{g}}{ \displaystyle \sum_{g=0}^{n} b^{g}}$$

106 |

Both $b$ and $n$ values can be changed. Base parameter controls relative impact of previous GWs. A higher value means higher weights for old GWs. Week parameter $n$ shows how many GWs are taken into consideration.

107 |
108 | 111 |
112 |
113 |
114 | 115 |
116 | 117 | 146 | 147 | 148 | "% with scripts=["main", "manager_form"] %" "% include 'footer.html' %" "% endwith %" 149 | -------------------------------------------------------------------------------- /archive/fpl_analytics_league.html: -------------------------------------------------------------------------------- 1 | "% include 'header.html' %" 2 | 3 | 4 |
5 | 6 |
7 |
8 |
9 | 10 |
11 |
12 | 17 |
18 |
19 | Snapshot: 20 | 25 | 26 |
27 |
28 | 33 |
34 | 35 | 36 | 37 |
38 | 39 | 40 |
41 |
42 |
43 | 44 |
45 |
46 |
47 |
FPL Analytics xP League
48 |
FPL Analytics League is generated using fplreview.com season review tool. DM on Twitter to add your team ID to the list.
49 |
50 | 51 |
Negative Variance Club
52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 98 | 99 | 100 |
PointsRanks
TwitterFPLxGIOMDLuckFPLxGIOMDLuck
{{ order+1 }}{{ i.twitter }}{{ parseInt(i.FPL) }}{{ parseFloat(i.xG).toFixed(2) }}{{ parseFloat(i.IO).toFixed(2) }}{{ parseFloat(i.MD).toFixed(2) }}{{ parseFloat(i.Luck).toFixed(2) }}{{ i.FPL_Rank }}{{ i.xG_Rank }}{{ i.IO_Rank }}{{ i.MD_Rank }}{{ i.Luck_Rank }}
101 |
102 | Loading data... Please wait... 103 | 104 |
105 |
106 |
107 |
108 |
109 | 110 |
111 |
112 |
113 |
Plots
114 | 115 |
116 |
117 |
118 |
FPL Points vs MD Points
119 |
120 | 121 |
122 |
123 |
124 |
FPL Points vs IO Points
125 |
126 | 127 |
128 |
129 |
130 |
FPL Rank vs MD Rank (Log)
131 |
132 | 133 |
134 |
135 |
136 |
FPL Rank vs IO Rank (Log)
137 |
138 | 139 |
140 |
141 |
142 | 143 |
144 |
145 |
146 |
147 | 148 | 149 | 171 | 172 | 173 |
174 | 175 | 176 | 177 | 178 | 179 | "% with scripts=["main", "fpl_analytics_league"] %" "% include 'footer.html' %" "% endwith %" -------------------------------------------------------------------------------- /src/static/js/ownership_rates.js: -------------------------------------------------------------------------------- 1 | var app = new Vue({ 2 | el: '#app', 3 | data: { 4 | this_gw: gw, 5 | elements: [], 6 | last_gw_elements: [], 7 | data_options: [ 8 | { 'name': "Official FPL API" } 9 | ], 10 | data_choice: 0, 11 | sample_data: {}, 12 | total_players: 1, 13 | custom_captaincy: {}, 14 | first_shown: true 15 | }, 16 | computed: { 17 | is_both_ready() { 18 | return !(_.isEmpty(this.elements) || _.isEmpty(this.last_gw_elements)) 19 | }, 20 | combined_elements() { 21 | if (!this.is_both_ready) { return []} 22 | let combined_data = _.cloneDeep(this.elements) 23 | let is_sample = false; 24 | let sample_data = []; 25 | let capt_rates = this.custom_captaincy; 26 | if (this.data_choice != 0 && !_.isEmpty(this.sample_data)) { 27 | is_sample = true; 28 | let sample_raw = get_ownership_by_type(this.data_options[this.data_choice].name, this.elements, this.sample_data, {}) 29 | sample_data = Object.fromEntries(sample_raw.data.map(i=>[i.id, i])) 30 | } 31 | combined_data.forEach((e) => { 32 | let match = _.find(this.last_gw_elements, {id: String(e.id)}) 33 | if (match == undefined) { 34 | e.last_selected_by_percent = 0; 35 | // e.trend = e.selected_by_percent; 36 | e.trend = 100 * (parseInt(e.transfers_in_event) - parseInt(e.transfers_out_event)) / parseInt(this.total_players); 37 | e.last_gw_final = 0; 38 | e.future_gw_final = e.trend; 39 | } 40 | else { 41 | let last_gw_own = match.selected_by_percent; 42 | e.last_selected_by_percent = last_gw_own; 43 | // e.trend = e.selected_by_percent - last_gw_own; 44 | e.trend = 100 * (parseInt(e.transfers_in_event) - parseInt(e.transfers_out_event)) / parseInt(this.total_players); 45 | if (is_sample) { 46 | let sample_match = sample_data[e.id]; 47 | if (sample_match == undefined) { 48 | e.last_gw_final = 0; 49 | e.future_gw_final = Math.min(100, Math.max(e.trend, 0)) 50 | } 51 | else { 52 | e.last_gw_final = parseFloat(sample_match.selected_by_percent); 53 | e.future_gw_final = Math.min(100, Math.max(e.last_gw_final + e.trend, 0)) 54 | } 55 | } 56 | else { 57 | e.last_gw_final = match.selected_by_percent; 58 | e.trend = e.selected_by_percent - e.last_gw_final; 59 | e.future_gw_final = e.selected_by_percent; 60 | } 61 | } 62 | if (e.id in capt_rates) { 63 | e.captaincy = parseFloat(capt_rates[e.id]); 64 | e.future_gw_final = e.future_gw_final + e.captaincy; 65 | } 66 | else { 67 | e.captaincy = 0; 68 | } 69 | 70 | }) 71 | 72 | setTimeout(() => { 73 | $("#ownership_rates_table").DataTable().destroy(); 74 | app.$nextTick(() => { 75 | $("#ownership_rates_table").DataTable({ 76 | "order": [ 77 | [4, 'desc'] 78 | ], 79 | "pageLength": 10, 80 | columnDefs: [ 81 | { orderable: false, targets: [1,2] } 82 | ], 83 | buttons: [ 84 | 'copy', 'csv' 85 | ] 86 | }); 87 | $("#ownership_rates_table").DataTable().buttons().container() 88 | .appendTo('#button-box'); 89 | app.first_shown = false; 90 | }); 91 | }, 100) 92 | return combined_data 93 | }, 94 | most_transferred_in() { 95 | if (_.isEmpty(this.elements)) { return []} 96 | let sorted_list = _.sortBy(this.elements, [(o) => {return -o.transfers_in_event}, (o) => {return -o.transfers_in}]); 97 | return sorted_list.slice(0, 10) 98 | }, 99 | most_transferred_out() { 100 | if (_.isEmpty(this.elements)) { return []} 101 | let sorted_list = _.sortBy(this.elements, [(o) => {return -o.transfers_out_event}, (o) => {return -o.transfers_out}]); 102 | return sorted_list.slice(0, 10) 103 | }, 104 | is_using_sample() { 105 | return this.data_choice != 0 106 | }, 107 | captaincy_ordered_list() { 108 | let ce = this.combined_elements; 109 | return _.sortBy(ce, [(o) => {return -parseFloat(o.selected_by_percent)}, (o) => {return -parseFloat(o.ep_next)}]) 110 | } 111 | }, 112 | methods: { 113 | saveMainData(data) { 114 | this.total_players = data.total_players 115 | this.elements = data.elements 116 | }, 117 | saveLastGWData(data) { 118 | this.last_gw_elements = data; 119 | }, 120 | saveSampleData(success, data) { 121 | if (success) { 122 | this.sample_data = data 123 | this.data_options = [ 124 | { 'name': "Official FPL API" }, 125 | { 'name': "Sample - Prime" }, 126 | { 'name': "Sample - Overall" }, 127 | { 'name': "Sample - Top 1M" }, 128 | { 'name': "Sample - Top 100K" }, 129 | { 'name': "Sample - Top 10K" }, 130 | { 'name': "Sample - Top 1K" }, 131 | { 'name': "Sample - Top 100" } 132 | ] 133 | this.data_choice = 1 134 | } 135 | }, 136 | invalidate_cache() { 137 | this.$nextTick(() => { 138 | // var table = $("#main_fixture").DataTable(); 139 | // table.cells("td").invalidate().draw(); 140 | // var table = $("#edit_fixture").DataTable(); 141 | // table.cells("td").invalidate().draw(); 142 | }) 143 | }, 144 | editCaptaincy(e) { 145 | let id = e.target.dataset.id; 146 | let value = e.target.value; 147 | this.$set(this.custom_captaincy, id, value) 148 | }, 149 | resetCaptaincy() { 150 | this.custom_captaincy = {}; 151 | jQuery("#captaincyModal").modal('hide') 152 | jQuery("#pasteModal").modal('hide') 153 | }, 154 | updateValues() { 155 | 156 | }, 157 | parsePasteAndClose() { 158 | this.custom_captaincy = {}; 159 | let values = jQuery("#paste_area").val() 160 | let clean_vals = values.replace(/\n/g, " ").split(" ").filter(i => i!='' && !i.includes('(') && !i.includes(')')) 161 | clean_vals.forEach((e,j) => { 162 | let match = this.elements.find(i => i.web_name == e) 163 | if (match == undefined) { 164 | return 165 | } 166 | else { 167 | let perc = parseFloat(clean_vals[j+1]) 168 | this.$set(this.custom_captaincy, parseInt(match.id), perc) 169 | } 170 | }) 171 | $("#paste_area").val('') 172 | jQuery("#pasteModal").modal('hide') 173 | } 174 | } 175 | }); 176 | 177 | async function fetch_fpl_main() { 178 | return get_fpl_main_data().then((data) => { 179 | app.saveMainData(data); 180 | }).catch((e) => { 181 | console.log("Error", e) 182 | }) 183 | } 184 | 185 | async function fetch_last_gw_data() { 186 | let v = listdates[0].split(' / ') 187 | console.log(v) 188 | return get_cached_element_data({season: v[0].trim(), gw: v[1].trim(), date: v[2].trim()}).then((data) => { 189 | app.saveLastGWData(data); 190 | }).catch((e) => { 191 | console.log("Error", e) 192 | }) 193 | } 194 | 195 | async function load_sample_data() { 196 | let v = listdates[0].split(' / ') 197 | return get_sample_data(v[0].trim(), v[1].slice(2)) 198 | .then((data) => { 199 | if (data[0].status == 'rejected') { 200 | app.saveSampleData(false, []); 201 | } 202 | else { 203 | let sample_data = data[0].value 204 | if (data[1].status != 'rejected') { 205 | sample_data['Prime'] = data[1].value 206 | } 207 | app.saveSampleData(true, sample_data); 208 | } 209 | 210 | // app.saveSampleData(true, data); 211 | }) 212 | .catch(error => { 213 | // Delete sample data and force official FPL API values 214 | app.saveSampleData(false, []); 215 | }); 216 | } 217 | 218 | $(document).ready(() => { 219 | Promise.all([ 220 | fetch_last_gw_data(), 221 | fetch_fpl_main(), 222 | load_sample_data() 223 | ]).then((values) => { 224 | app.$nextTick(() => { 225 | console.log('READY!') 226 | }) 227 | }) 228 | .catch((error) => { 229 | console.error("An error has occured: " + error); 230 | }); 231 | $('#captaincyModal').on('shown.bs.modal', function(e) { 232 | $("#captainTable").DataTable().destroy() 233 | app.$nextTick(() => { 234 | $("#captainTable").DataTable({ 235 | "order": [], 236 | "pageLength": 10, 237 | "info": false 238 | }) 239 | }) 240 | }) 241 | }) -------------------------------------------------------------------------------- /src/static/images/jersey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 32 | 33 | 34 | 54 | 56 | 57 | 59 | image/svg+xml 60 | 62 | 63 | 64 | 65 | 66 | 71 | 74 | 79 | 81 | 86 | 91 | 92 | 95 | 100 | 105 | 106 | 111 | 115 | 118 | 9.99 135 | 146 | 153 | 160 | Aubameyang 171 | Arsenal 182 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /src/static/css/svg.css: -------------------------------------------------------------------------------- 1 | .field-common { 2 | opacity: 1; 3 | fill: none; 4 | fill-opacity: 1; 5 | stroke: #ababab; 6 | stroke-width: 1.85208333; 7 | stroke-miterlimit: 4; 8 | stroke-dasharray: none; 9 | stroke-opacity: 1; 10 | } 11 | 12 | .field-last-path { 13 | stroke-linecap: butt; 14 | stroke-linejoin: miter; 15 | } 16 | 17 | .jersey-regular { 18 | fill: none; 19 | stroke: #000000; 20 | stroke-width: 0.26458332px; 21 | stroke-linecap: butt; 22 | stroke-linejoin: miter; 23 | stroke-opacity: 1; 24 | } 25 | 26 | .jersey-warning { 27 | fill: #ff8787 !important; 28 | } 29 | 30 | .jersey-warning-inner { 31 | fill: #a5a5a5 !important; 32 | } 33 | 34 | .jersey-team-text { 35 | fill: #B81636; 36 | } 37 | 38 | .jersey-regular-text { 39 | font-style: normal; 40 | font-variant: normal; 41 | font-weight: normal; 42 | font-stretch: normal; 43 | font-size: 10.58333302px; 44 | line-height: 1.25; 45 | font-family: sans-serif; 46 | font-variant-ligatures: normal; 47 | font-variant-caps: normal; 48 | font-variant-numeric: normal; 49 | font-feature-settings: normal; 50 | text-align: center; 51 | letter-spacing: 0px; 52 | word-spacing: 0px; 53 | writing-mode: lr-tb; 54 | text-anchor: middle; 55 | fill: #000000; 56 | fill-opacity: 1; 57 | stroke: none; 58 | stroke-width: 0.26458332; 59 | } 60 | 61 | .jersey-xp-text { 62 | font-style: normal; 63 | font-variant: normal; 64 | font-weight: normal; 65 | font-stretch: normal; 66 | font-size: 10.58333302px; 67 | font-family: sans-serif; 68 | font-variant-ligatures: normal; 69 | font-variant-caps: normal; 70 | font-variant-numeric: normal; 71 | font-feature-settings: normal; 72 | text-align: center; 73 | writing-mode: lr-tb; 74 | text-anchor: middle; 75 | stroke-width: 0.26458332; 76 | } 77 | 78 | .jersey-captain-star { 79 | font-style: normal; 80 | font-variant: normal; 81 | font-weight: normal; 82 | font-stretch: normal; 83 | font-size: 6px; 84 | font-family: sans-serif; 85 | font-variant-ligatures: normal; 86 | font-variant-caps: normal; 87 | font-variant-numeric: normal; 88 | font-feature-settings: normal; 89 | text-align: center; 90 | writing-mode: lr-tb; 91 | text-anchor: middle; 92 | stroke-width: 0.26458332; 93 | } 94 | 95 | .jersey-name-bg { 96 | opacity: 0.22000002; 97 | stroke: #ababab; 98 | stroke-width: 0; 99 | stroke-miterlimit: 4; 100 | stroke-dasharray: none; 101 | stroke-opacity: 1; 102 | fill: aquamarine !important; 103 | fill-opacity: 1 !important; 104 | } 105 | 106 | .jersey-team-bg { 107 | stroke: #ababab; 108 | stroke-width: 0; 109 | stroke-miterlimit: 4; 110 | stroke-dasharray: none; 111 | stroke-opacity: 1; 112 | fill: white !important; 113 | fill-opacity: 1 !important; 114 | opacity: 0.9 !important; 115 | } 116 | 117 | .jersey-xp-bg { 118 | stroke: #ababab; 119 | stroke-width: 0; 120 | stroke-miterlimit: 4; 121 | stroke-dasharray: none; 122 | stroke-opacity: 1; 123 | fill: #484848 !important; 124 | fill-opacity: 1 !important; 125 | opacity: 0.9 !important; 126 | } 127 | 128 | .jersey-xp-avg { 129 | /* text-anchor: middle; */ 130 | font-size: 1.8pt; 131 | stroke: none; 132 | alignment-baseline: central; 133 | fill: #ffffff; 134 | } 135 | 136 | .jersey-xp-tag { 137 | fill: #8fffa9; 138 | alignment-baseline: central; 139 | dominant-baseline: central; 140 | } 141 | 142 | .jersey-player-name { 143 | fill: white; 144 | font-style: normal; 145 | font-variant: normal; 146 | font-weight: normal; 147 | font-stretch: normal; 148 | font-size: 2.46944451px; 149 | font-family: sans-serif; 150 | font-variant-ligatures: normal; 151 | font-variant-caps: normal; 152 | font-variant-numeric: normal; 153 | font-feature-settings: normal; 154 | text-align: center; 155 | writing-mode: lr-tb; 156 | text-anchor: middle; 157 | stroke-width: 0.26458332; 158 | } 159 | 160 | .jersey-player-name>tspan { 161 | font-size: 2.8px !important; 162 | } 163 | 164 | .jersey-team-name { 165 | fill: #a10a28 !important; 166 | font-style: normal; 167 | font-variant: normal; 168 | font-weight: normal; 169 | font-stretch: normal; 170 | font-size: 2.11666656px; 171 | font-family: sans-serif; 172 | font-variant-ligatures: normal; 173 | font-variant-caps: normal; 174 | font-variant-numeric: normal; 175 | font-feature-settings: normal; 176 | text-align: center; 177 | writing-mode: lr-tb; 178 | text-anchor: middle; 179 | stroke-width: 0.26458332; 180 | } 181 | 182 | .jersey-points-display { 183 | fill: wheat !important; 184 | font-style: normal; 185 | font-variant: normal; 186 | font-weight: normal; 187 | font-stretch: normal; 188 | font-size: 2.11666656px; 189 | font-family: sans-serif; 190 | font-variant-ligatures: normal; 191 | font-variant-caps: normal; 192 | font-variant-numeric: normal; 193 | font-feature-settings: normal; 194 | text-align: center; 195 | writing-mode: lr-tb; 196 | text-anchor: middle; 197 | stroke-width: 0.26458332; 198 | } 199 | 200 | .jersey-cost-text { 201 | font-size: 3px; 202 | stroke: none; 203 | stroke-width: 0; 204 | fill: rgb(59, 117, 98); 205 | text-align: center; 206 | text-anchor: middle; 207 | } 208 | 209 | .jersey-subs-button { 210 | fill-opacity: 1; 211 | fill: #ffffff !important; 212 | stroke: #ffffff !important; 213 | stroke-opacity: 1; 214 | stroke-width: 0.1; 215 | cursor: pointer; 216 | } 217 | 218 | .jersey-subs-text { 219 | font-size: 3.6px; 220 | fill: #4e00fb !important; 221 | stroke: #000000 !important; 222 | stroke-width: 0.1px; 223 | text-anchor: middle; 224 | alignment-baseline: middle; 225 | pointer-events: none; 226 | } 227 | 228 | .jersey-transfer-button { 229 | fill-opacity: 1; 230 | fill: #d6d6d6 !important; 231 | stroke: #ffffff !important; 232 | stroke-opacity: 1; 233 | stroke-width: 0.1; 234 | cursor: pointer; 235 | } 236 | 237 | .jersey-transfer-text { 238 | font-size: 3px; 239 | fill: #901f1f !important; 240 | stroke: none !important; 241 | text-anchor: middle; 242 | alignment-baseline: middle; 243 | pointer-events: none; 244 | } 245 | 246 | rect.game_bar { 247 | fill: black; 248 | opacity: 0.025; 249 | pointer-events: none; 250 | } 251 | 252 | rect.player_point_bar { 253 | fill: #ffffff; 254 | } 255 | 256 | rect.lineup_bar { 257 | fill: #6fcfd6; 258 | opacity: 0.3; 259 | pointer-events: none; 260 | } 261 | 262 | rect.bench_bar { 263 | fill: #b995d2; 264 | opacity: 0.3; 265 | pointer-events: none; 266 | } 267 | 268 | .close_text { 269 | pointer-events: none; 270 | } 271 | 272 | .close_box { 273 | cursor: pointer; 274 | } 275 | 276 | .candle { 277 | /* opacity: 0.9 */ 278 | } 279 | 280 | .candle:hover { 281 | opacity: 1; 282 | stroke: #ff9500; 283 | stroke-width: 1px; 284 | } 285 | 286 | circle.risk-nodes { 287 | fill: #579e00; 288 | stroke: white; 289 | } 290 | 291 | circle.risk-nodes:hover { 292 | stroke: #ffb10f; 293 | } 294 | 295 | .candle-gain { 296 | fill: #6fcfd6; 297 | } 298 | .candle-loss { 299 | fill: #de6363; 300 | } 301 | 302 | .svg-legend-text { 303 | font-size: small; 304 | } 305 | 306 | .gw-bg-odd { 307 | fill: white; 308 | opacity: 0.1; 309 | } 310 | 311 | .gw-bg-even { 312 | fill: none; 313 | opacity: 0; 314 | } 315 | 316 | .svg-circles:hover { 317 | opacity:1 318 | } 319 | 320 | .owned-circle { 321 | fill:rgb(111, 207, 214); 322 | stroke:white; 323 | opacity: 0.7; 324 | stroke-width: 0.2; 325 | cursor: pointer; 326 | } 327 | 328 | .nonowned-circle { 329 | fill:rgb(255 83 83); 330 | stroke:white; 331 | opacity: 0.3; 332 | stroke-width: 0.2; 333 | cursor: pointer; 334 | } 335 | 336 | .analytics_circles { 337 | fill: #ff8f8f; 338 | stroke:white; 339 | opacity: 0.4; 340 | stroke-width: 0.2; 341 | cursor: pointer; 342 | } 343 | 344 | .analytics_circles:hover { 345 | opacity: 1; 346 | } 347 | 348 | .selected-circle { 349 | fill: #31fff9; 350 | stroke: white; 351 | opacity: 1; 352 | } 353 | 354 | .empty_captain { 355 | fill: white !important; 356 | fill-opacity: 0 !important; 357 | cursor: pointer; 358 | stroke: white !important; 359 | stroke-opacity: 0.5; 360 | stroke-width: 0.4pt !important; 361 | } 362 | 363 | .current_captain { 364 | fill: #00837e !important; 365 | fill-opacity: 1 !important; 366 | cursor: pointer; 367 | stroke: palegreen !important; 368 | stroke-width: 0.4pt !important; 369 | } 370 | 371 | .current_vcaptain { 372 | fill: #830000 !important; 373 | fill-opacity: 1 !important; 374 | cursor: pointer; 375 | stroke: #fb9898 !important; 376 | stroke-width: 0.4pt !important; 377 | } 378 | 379 | .captain_text { 380 | stroke-width: 0 !important; 381 | font-size: 2.4pt; 382 | pointer-events: none; 383 | } 384 | 385 | .empty_out { 386 | fill: white !important; 387 | fill-opacity: 0 !important; 388 | cursor: pointer; 389 | stroke: white !important; 390 | stroke-opacity: 0.2; 391 | stroke-width: 0.4pt !important; 392 | } 393 | 394 | .selected_out { 395 | fill: #e30000 !important; 396 | fill-opacity: 1 !important; 397 | cursor: pointer; 398 | stroke: #ffffff !important; 399 | stroke-opacity: 1; 400 | stroke-width: 0.4pt !important; 401 | } 402 | 403 | .out_text { 404 | stroke-width: 0 !important; 405 | font-size: 1.8pt; 406 | font-weight: bold; 407 | pointer-events: none; 408 | } 409 | 410 | .half-opacity { 411 | opacity: 0.5 !important; 412 | } 413 | 414 | .field_icons { 415 | font-family: sans-serif; 416 | font-weight: bold; 417 | text-anchor: middle; 418 | alignment-baseline: central; 419 | dominant-baseline: central; 420 | } 421 | 422 | .calculating { 423 | pointer-events: none; 424 | } 425 | 426 | .comparison-xp-lines { 427 | color: gray; 428 | opacity: 0.3; 429 | } 430 | 431 | .comparison-xp-domain { 432 | opacity: 0; 433 | } 434 | 435 | .allow-mix { 436 | mix-blend-mode: lighten; 437 | } 438 | 439 | .chip-text { 440 | font-size: 5pt; 441 | font-weight: bold; 442 | text-shadow: 1px 1px 1px black; 443 | } 444 | 445 | .comp-name-text { 446 | fill: white; 447 | font-weight: bold; 448 | text-shadow: 0px 0px 2px black, 0px 0px 2px black, 0px 0px 2px black; 449 | } 450 | 451 | .tc-on-field { 452 | font-size: 1.5pt; 453 | fill: #9af4ff !important; 454 | } 455 | -------------------------------------------------------------------------------- /src/static/js/sampling_utils.js: -------------------------------------------------------------------------------- 1 | let compactFormatter = Intl.NumberFormat('en', { notation: 'compact' }); 2 | 3 | function sample_compact_number(value) { 4 | switch (value) { 5 | case "Overall": 6 | return "Overall"; 7 | case "100": 8 | return "Top 100"; 9 | case "1000": 10 | return "Top 1K"; 11 | case "10000": 12 | return "Top 10K"; 13 | case "100000": 14 | return "Top 100K"; 15 | case "1000000": 16 | return "Top 1M"; 17 | case "Prime": 18 | return "Prime"; 19 | default: 20 | return value; 21 | // let new_value = compactFormatter.format(value); 22 | // return new_value !== "NaN" ? ("Top " + new_value) : value; 23 | } 24 | } 25 | 26 | function reverse_sample_name(value) { 27 | switch (value) { 28 | case "Sample - Overall": 29 | return "Overall"; 30 | case "FPL Data": 31 | return "Official FPL API" 32 | case "Sample - Top 100": 33 | case "Top 100": 34 | return 100; 35 | case "Sample - Top 1K": 36 | case "Top 1K": 37 | return 1000; 38 | case "Sample - Top 10K": 39 | case "Top 10K": 40 | return 10000; 41 | case "Sample - Top 100K": 42 | case "Top 100K": 43 | return 100000; 44 | case "Sample - Top 1M": 45 | case "Top 1M": 46 | return 1000000; 47 | case "Sample - Prime": 48 | case "Prime": 49 | return "Prime"; 50 | default: 51 | return value.includes("Sample - ") ? value.replace("Sample - ", "") : value; 52 | } 53 | } 54 | 55 | function autosubbed_team(team_picks, autosub_dict) { 56 | 57 | let sub_replacements = [] 58 | let cap_replacements = [] 59 | let split_team = _.groupBy(team_picks, (i) => i.multiplier > 0) 60 | let lineup = split_team[true] 61 | let bench = split_team[false] || [] 62 | for (let i of lineup) { 63 | let id = i.element; 64 | let info = autosub_dict[id]; 65 | if (info === undefined) { 66 | continue; 67 | } 68 | if (info.autosub) { 69 | let original_mult = i.multiplier; 70 | i.multiplier = 0; 71 | if (i.is_captain) { 72 | i.is_captain = false; 73 | let vc = lineup.find(j => j.is_vice_captain && autosub_dict[j.element].autosub == false); 74 | if (vc && vc.multiplier > 0) { 75 | vc.is_captain = true; 76 | vc.is_vice_captain = false; 77 | if (vc.multiplier <= 1) { 78 | vc.multiplier = original_mult; 79 | } 80 | cap_replacements.push([id, vc.element]) 81 | } 82 | } 83 | let target_pos = info.element_type; 84 | let current_cnt = team_picks.filter(j => j.multiplier > 0 && (autosub_dict[j.element] && autosub_dict[j.element].element_type == target_pos)).length; 85 | if (element_type[target_pos].min > current_cnt || target_pos == "1") { 86 | // only this type 87 | let player_to_enter = bench.find(j => autosub_dict[j.element] && autosub_dict[j.element].element_type == target_pos && autosub_dict[j.element].autosub == false) 88 | if (player_to_enter) { 89 | player_to_enter.multiplier = 1; 90 | bench = bench.filter(i => i.element != player_to_enter.element) 91 | sub_replacements.push([id, player_to_enter.element]) 92 | } 93 | } else { 94 | // anyone on bench 95 | let player_to_enter = bench.find(j => autosub_dict[j.element] && autosub_dict[j.element].element_type != "1" && autosub_dict[j.element].autosub == false) 96 | if (player_to_enter) { 97 | player_to_enter.multiplier = 1; 98 | bench = bench.filter(i => i.element != player_to_enter.element) 99 | sub_replacements.push([id, player_to_enter.element]) 100 | } 101 | } 102 | } 103 | } 104 | return { 'team': team_picks, 'sub_replacement': sub_replacements, 'cap_replacement': cap_replacements }; 105 | } 106 | 107 | function prepare_fixture_data(data) { 108 | 109 | data.forEach((game, index) => { 110 | game.start_dt = new Date(game.kickoff_time); 111 | game.end_dt = new Date(game.start_dt.getTime() + (105 * 60 * 1000)); 112 | game.node_info = { start: (game.start_dt).getTime(), end: game.end_dt.getTime(), content: 'Game' } 113 | }) 114 | 115 | data.sort((a, b) => { return a.node_info.start - b.node_info.start }); 116 | 117 | data.forEach((game, index) => { 118 | game.duration = 105 * 60 * 1000; 119 | game.team_h_name = teams_ordered[game.team_h - 1].name; 120 | game.team_a_name = teams_ordered[game.team_a - 1].name; 121 | game.label = teams_ordered[game.team_h - 1].name + " vs " + teams_ordered[game.team_a - 1].name; 122 | let order = 0; 123 | data.slice(0, index).forEach((game2) => { 124 | if ((game.start_dt >= game2.start_dt && game.start_dt <= game2.end_dt) || 125 | (game.end_dt >= game2.start_dt && game.end_dt <= game2.end_dt)) { 126 | if (game2.order == order) { 127 | order += 1; 128 | } 129 | } 130 | }) 131 | game.order = order; 132 | }) 133 | 134 | return data; 135 | } 136 | 137 | 138 | function get_provisional_bonus(gw_fixture) { 139 | let bonus_players = {}; 140 | 141 | gw_fixture.forEach((game) => { 142 | let bps_provisional = {}; 143 | 144 | if (game.started && !game.finished) { 145 | try { 146 | let bps_stats = game.stats.find(i => i.identifier == "bps") 147 | let all_players = bps_stats.h.concat(bps_stats.a) 148 | let sorted_groups = Object.entries(_.groupBy(all_players, i => i.value)).sort((a, b) => b[0] - a[0]).slice(0, 3) 149 | sorted_groups.forEach((cat, i) => { 150 | let bonus = 3 - i; 151 | cat[1].forEach((p) => { 152 | bonus_players[p.element] = bonus; 153 | }); 154 | }) 155 | } catch (err) {} 156 | } 157 | }) 158 | 159 | return bonus_players; 160 | } 161 | 162 | function rp_by_id_dict(fixture, rp_data) { 163 | rp_data.forEach((p) => { 164 | try { 165 | p.games_finished = p.explain.map(i => { 166 | let f = fixture.find(j => j.id == i.fixture) 167 | if (f == undefined) { return true } 168 | return f.finished_provisional 169 | }).every(i => i); 170 | if (p.games_finished && p.stats.minutes == 0) { 171 | p.autosub = true; 172 | } else { 173 | p.autosub = false; 174 | } 175 | } catch (e) { 176 | console.log("Player game_finished error", e) 177 | } 178 | }) 179 | let rp_obj = Object.fromEntries(_.cloneDeep(rp_data.map(i => [i.id, i]))); 180 | if (!_.isEmpty(this.provisional_bonus) && !_.isEmpty(rp_obj)) { 181 | Object.entries(this.provisional_bonus).forEach(entry => { 182 | const [key, value] = entry; 183 | rp_obj[key].stats.total_points += value; 184 | }) 185 | } 186 | return rp_obj; 187 | } 188 | 189 | function generate_autosub_dict(el_data, rp_by_id) { 190 | let autosubs = []; 191 | el_data.forEach((e) => { 192 | autosubs.push([e.id, { element_type: e.element_type, autosub: rp_by_id[e.id] ? rp_by_id[e.id].autosub : false }]); 193 | }) 194 | let autosub_dict = Object.fromEntries(autosubs); 195 | return autosub_dict; 196 | } 197 | 198 | function get_ownership_by_type(ownership_source, fpl_data, sample_data, autosubs) { 199 | 200 | let teams = []; 201 | if (sample_data == undefined) { 202 | ownership_source = "Official FPL API"; 203 | } else if (Object.keys(sample_data).length == 0) { 204 | ownership_source = "Official FPL API"; 205 | } 206 | 207 | let tag = reverse_sample_name(ownership_source) 208 | if (tag == "Official FPL API") { 209 | return {data: fpl_data} 210 | } 211 | 212 | if (tag in sample_data) { 213 | teams = sample_data[tag].filter(i => i.team != undefined) 214 | } 215 | else { 216 | teams = sample_data['Overall'].filter(i => i.team != undefined) 217 | } 218 | 219 | 220 | let el_copy = _.cloneDeep(fpl_data); 221 | let sub_replacements = []; 222 | let cap_replacements = []; 223 | 224 | teams = teams.filter(i => i.data != null) 225 | 226 | if (!_.isEmpty(autosubs)) { 227 | teams = _.cloneDeep(teams); 228 | teams.forEach((team) => { 229 | let edits = autosubbed_team(team.data.picks, autosubs); 230 | team.data.picks = edits.team; 231 | sub_replacements = sub_replacements.concat(edits.sub_replacement) 232 | cap_replacements = cap_replacements.concat(edits.cap_replacement) 233 | }) 234 | } 235 | 236 | let all_players = teams.map(i => i.data.picks).flat(); 237 | let grouped_players = _.groupBy(all_players, (i) => i.element); 238 | 239 | el_copy.forEach((e) => { 240 | // let this_player_picks = all_players.filter(i => i.element == e.id); 241 | let this_player_picks = grouped_players[e.id]; 242 | if (this_player_picks !== undefined) { 243 | let cnt = this_player_picks.length; 244 | e.selected_by_percent = cnt / teams.length * 100; 245 | let sum_of_multiplier = getSum(this_player_picks.map(i => i.multiplier)); 246 | e.effective_ownership = sum_of_multiplier / teams.length * 100; 247 | let captain_count = this_player_picks.filter(i => i.multiplier > 1.5).length; 248 | e.captain_percentage = captain_count / teams.length * 100; 249 | } else { 250 | e.selected_by_percent = 0; 251 | e.effective_ownership = 0; 252 | e.captain_percentage = 0; 253 | } 254 | 255 | }); 256 | return { data: el_copy, sub_replacement: sub_replacements, cap_replacement: cap_replacements }; 257 | } 258 | 259 | async function get_latest_sample_data(season, gw) { 260 | return new Promise((resolve, reject) => { 261 | get_sample_data(season, gw.slice(2)).then(data => { 262 | if (data[0].status == 'rejected') { 263 | get_sample_data(season, parseInt(gw.slice(2))-1).then((data) => { 264 | if (data[0].status == 'rejected') { 265 | reject("no data") 266 | } 267 | else { 268 | let sample_data = data[0].value 269 | if (data[1].status != 'rejected') { 270 | sample_data['Prime'] = data[1].value 271 | } 272 | resolve({gw: parseInt(gw.slice(2))-1, data: sample_data}) 273 | } 274 | }) 275 | } 276 | else { 277 | let sample_data = data[0].value 278 | if (data[1].status != 'rejected') { 279 | sample_data['Prime'] = data[1].value 280 | } 281 | resolve({gw: gw.slice(2), data: sample_data}) 282 | } 283 | }) 284 | }) 285 | } 286 | -------------------------------------------------------------------------------- /src/templates/impact_summary.html: -------------------------------------------------------------------------------- 1 | "% include 'header.html' %" 2 | 3 | 4 | 5 |
6 | 7 |
8 |
9 |
10 |
Impact Summary
11 |
12 | Impact of players and teams on your FPL season 13 |
14 |
15 | 16 | under development!
17 | content goes here! 18 | 19 | 119 |
120 |
121 |
122 |
123 | 124 |
125 |
126 |
127 |
128 |
Q/A
129 | X? 130 |

Y.

131 |
132 | 135 |
136 |
137 |
138 | 139 |
140 | 141 | 191 | 192 | "% with scripts=["main", "sampling_utils", "impact_summary"] %" "% include 'footer.html' %" "% endwith %" --------------------------------------------------------------------------------