PageSourceSearch

https://fploptimized.com/static/js/main.js?ts=20260920183004

js fploptimized.com collected 2026-10-04 13:32:07 UTC 28,803 bytes, 1,027 lines download raw bytes

1// Edge fixes
2
3Object.fromEntries =
4  Object.fromEntries ||
5  function (arr) {
6    return arr.reduce(function (acc, curr) {
7      acc[curr[0]] = curr[1];
8      return acc;
9    }, {});
10  };
11
12let proxy = "https://cors2.fploptimized.com";
13// let proxy = "https://cors.alpscode.com"
14// let proxy = "https://cors.fploptimized.com"
15
16if (!Array.prototype.flat) {
17  Object.defineProperty(Array.prototype, "flat", {
18    value: function (depth = 1, stack = []) {
19      for (let item of this) {
20        if (item instanceof Array && depth > 0) {
21          item.flat(depth - 1, stack);
22        } else {
23          stack.push(item);
24        }
25      }
26      return stack;
27    },
28  });
29}
30
31if (!Promise.allSettled) {
32  function allSettled(promises) {
33    let wrappedPromises = promises.map((p) =>
34      Promise.resolve(p).then(
35        (val) => ({ status: "fulfilled", value: val }),
36        (err) => ({ status: "rejected", reason: err })
37      )
38    );
39    return Promise.all(wrappedPromises);
40  }
41  Promise.allSettled = allSettled;
42}
43
44let team_codes = {
45  3: { name: "Arsenal", short: "ARS" },
46  7: { name: "Aston Villa", short: "AVL" },
47  91: { name: "Bournemouth", short: "BOU" },
48  94: { name: "Brentford", short: "BRE" },
49  36: { name: "Brighton", short: "BHA" },
50  90: { name: "Burnley", short: "BUR" },
51  8: { name: "Chelsea", short: "CHE" },
52  31: { name: "Crystal Palace", short: "CRY" },
53  11: { name: "Everton", short: "EVE" },
54  54: { name: "Fulham", short: "FUL" },
55  2: { name: "Leeds", short: "LEE" },
56  40: { name: "Ipswich", short: "IPS" },
57  13: { name: "Leicester", short: "LEI" },
58  14: { name: "Liverpool", short: "LIV" },
59  43: { name: "Man City", short: "MCI" },
60  1: { name: "Man Utd", short: "MUN" },
61  4: { name: "Newcastle", short: "NEW" },
62  17: { name: "Nott'm Forest", short: "NFO" },
63  56: { name: "Sunderland", short: "SUN" },
64  45: { name: "Norwich", short: "NOR" },
65  49: { name: "Sheffield Utd", short: "SHU" },
66  20: { name: "Southampton", short: "SOU" },
67  6: { name: "Spurs", short: "TOT" },
68  57: { name: "Watford", short: "WAT" },
69  35: { name: "West Brom", short: "WBA" },
70  21: { name: "West Ham", short: "WHU" },
71  39: { name: "Wolves", short: "WOL" },
72  102: { name: "Luton", short: "LUT" },
73  9: { name: "Coventry", short: "COV" },
74  88: { name: "Hull", short: "HUL" },
75};
76
77let team_short_dict = _.fromPairs(
78  _(team_codes)
79    .map((val, key) => [val.short, key])
80    .value()
81);
82
83let season_teams = [
84  "ARS",
85  "AVL",
86  "BOU",
87  "BRE",
88  "BHA",
89  "CHE",
90  "COV",
91  "CRY",
92  "EVE",
93  "FUL",
94  "HUL",
95  "IPS",
96  "LEE",
97  "LIV",
98  "MCI",
99  "MUN",
100  "NEW",
101  "NFO",
102  "SUN",
103  "TOT",
104];
105
106// Index must match the FPL team id (teams_ordered[team_id - 1] is used
107// throughout), which FPL assigns by sorting its `name` field -- note that
108// puts Spurs before Sunderland. Regenerate from bootstrap-static each season.
109let teams_ordered = [
110  { name: "Arsenal", short: "ARS" },
111  { name: "Aston Villa", short: "AVL" },
112  { name: "Bournemouth", short: "BOU", long: "AFC Bournemouth" },
113  { name: "Brentford", short: "BRE" },
114  { name: "Brighton", short: "BHA", long: "Brighton and Hove Albion" },
115  { name: "Chelsea", short: "CHE" },
116  { name: "Coventry", short: "COV", long: "Coventry City" },
117  { name: "Crystal Palace", short: "CRY" },
118  { name: "Everton", short: "EVE" },
119  { name: "Fulham", short: "FUL" },
120  { name: "Hull", short: "HUL", long: "Hull City" },
121  { name: "Ipswich", short: "IPS", long: "Ipswich Town" },
122  { name: "Leeds", short: "LEE", long: "Leeds United" },
123  { name: "Liverpool", short: "LIV" },
124  { name: "Man City", short: "MCI", long: "Manchester City" },
125  { name: "Man Utd", short: "MUN", long: "Manchester United" },
126  { name: "Newcastle", short: "NEW" },
127  { name: "Nott'm Forest", short: "NFO", long: "Nottingham Forest" },
128  { name: "Spurs", short: "TOT", long: "Tottenham Hotspur" },
129  { name: "Sunderland", short: "SUN" },
130];
131
132let pl_team_colors = {
133  ARS: ["#EF0107", "#FFFFFF"],
134  AVL: ["#95BFE5", "#670E36"],
135  BRE: ["#FFFFFF", "#e30613"],
136  BHA: ["#FFFFFF", "#40a8ff"],
137  BUR: ["#6C1D45", "#99D6EA"],
138  CHE: ["#034694", "#D1D3D4"],
139  CRY: ["#1B458F", "#D16475"],
140  EVE: ["#003399", "#FFFFFF"],
141  LEI: ["#003090", "#FDBE11"],
142  LEE: ["#FFFFFF", "#0030b9"],
143  LIV: ["#C8102E", "#0bcdc3"],
144  MCI: ["#6CABDD", "#1C2C5B"],
145  MUN: ["#DA291C", "#FBE122"],
146  NEW: ["#241F20", "#FFFFFF"],
147  NOR: ["#FFF200", "#00A650"],
148  SOU: ["#ed1a3b", "#FFFFFF"],
149  TOT: ["#132257", "#FFFFFF"],
150  WAT: ["#FBEE23", "#ED2127"],
151  WHU: ["#7A263A", "#F3D459"],
152  WOL: ["#FDB913", "#231F20"],
153  FUL: ["#000000", "#FFFFFF"],
154  BOU: ["#DA291C", "#EFDBB2"],
155  NFO: ["#E53233", "#FFFFFF"],
156  LUT: ["#F78F1E", "#002D62"],
157  SHU: ["#EE2737", "#000000"],
158  IPS: ["#DE2C37", "#3A64A3"],
159  WBA: ["#122F67", "#FFFFFF"],
160  SUN: ["#E10600", "#FFFFFF"],
161  COV: ["#78D0F3", "#1C2C5B"],
162  HUL: ["#F5A12D", "#000000"],
163};
164
165// Defensive contribution banks 2 points once a player reaches the threshold
166// for their position: 10 for defenders (clearances, blocks, interceptions,
167// tackles), 12 for midfielders and forwards, whose count also includes ball
168// recoveries. Goalkeepers are not eligible.
169let defcon_thresholds = { 2: 10, 3: 12, 4: 12 };
170
171let element_type = {
172  1: { name: "Goalkeeper", short: "GK", id: 1, min: 1, max: 1, cnt: 2 },
173  2: { name: "Defender", short: "DF", id: 2, min: 3, max: 5, cnt: 5 },
174  3: { name: "Midfielder", short: "MD", id: 3, min: 2, max: 5, cnt: 5 },
175  4: { name: "Forward", short: "FW", id: 4, min: 1, max: 3, cnt: 3 },
176  // 5: { name: "Managers ", short: "MNG", id: 5, min: 0, max: 1, cnt: 1 },
177};
178
179let stat_types = {
180  goals_scored: { name: "Goals", icon: "fas fa-futbol" },
181  assists: { name: "Assists", icon: "fas fa-hands-helping" },
182  bonus: { name: "Bonus", icon: "fas fa-coins" },
183  bps: { name: "BPS" },
184  own_goals: { name: "Own Goals", icon: "far fa-grimace" },
185  penalties_missed: { name: "Penalties Missed", icon: "far fa-thumbs-down" },
186  penalties_saved: { name: "Penalties Saved", icon: "fas fa-chess-rook" },
187  saves: { name: "Saves", icon: "far fa-hand-paper" },
188  red_cards: { name: "Red Cards", icon: "fas fa-door-closed" },
189  yellow_cards: { name: "Yellow Cards", icon: "fas fa-door-open" },
190  bps_provisional: { name: "BPS (Live)", icon: "fas fa-calculator" },
191  defensive_contribution: { name: "Def Con", icon: "fas fa-shield-alt" },
192  // mng_win: { name: "AM Win", icon: "fas fa-trophy" },
193  // mng_draw: { name: "AM Draw", icon: "fas fa-trophy" },
194  // mng_loss: { name: "AM Loss", icon: "fas fa-trophy" },
195  // mng_underdog_win: { name: "AM Underdog Win", icon: "fas fa-trophy" },
196  // mng_underdog_draw: { name: "AM Underdog Draw", icon: "fas fa-trophy" },
197  // mng_clean_sheets: { name: "AM Clean Sheet", icon: "fas fa-trophy" },
198  // mng_goals_scored: { name: "AM Goals Scored", icon: "fas fa-trophy" },
199};
200
201let player_stat_types = {
202  minutes: { name: "Minutes", pp: true },
203  clean_sheets: { name: "Clean Sheets", pp: false },
204  goals_scored: { name: "Goals", pp: true },
205  assists: { name: "Assists", pp: true },
206  bonus: { name: "Bonus", pp: true },
207  defensive_contribution: { name: "Def Con", pp: true },
208  saves: { name: "Saves", pp: true },
209  penalties_saved: { name: "Penalties Saved", pp: false },
210  goals_conceded: { name: "Goals Conceded", pp: true },
211  own_goals: { name: "Own Goals", pp: false },
212  yellow_cards: { name: "Yellow Cards", pp: false },
213  red_cards: { name: "Red Cards", pp: false },
214  penalties_missed: { name: "Penalties Missed", pp: false },
215  // mng_win: { name: "AM Win", pp: false },
216  // mng_draw: { name: "AM Draw", pp: false },
217  // mng_loss: { name: "AM Loss", pp: false },
218  // mng_underdog_win: { name: "AM Underdog Win", pp: false },
219  // mng_underdog_draw: { name: "AM Underdog Draw", pp: false },
220  // mng_clean_sheets: { name: "AM Clean Sheets", pp: false },
221  // mng_goals_scored: { name: "AM Goals Scored", pp: false },
222};
223
224function rounded(val, digits = 2) {
225  if (val === undefined || val === "") {
226    return "-";
227  } else {
228    return parseFloat(val).toFixed(digits);
229  }
230}
231
232function getWithSign(val, digits = 2) {
233  if (val === undefined) {
234    return "";
235  }
236  if (val >= 0) {
237    return "+" + parseFloat(val).toFixed(digits);
238  } else {
239    return val.toFixed(digits);
240  }
241}
242
243function getSum(arr) {
244  return arr.reduce((a, b) => a + b, 0);
245}
246
247function getAvg(arr) {
248  return arr.reduce((a, b) => a + b, 0) / Math.max(arr.length, 1);
249}
250
251function getMin(arr) {
252  return Math.min(...arr);
253}
254
255function getMax(arr) {
256  return Math.max(...arr);
257}
258
259const downloadToFile = (content, filename, contentType) => {
260  const a = document.createElement("a");
261  const file = new Blob([content], { type: contentType });
262
263  a.href = URL.createObjectURL(file);
264  a.download = filename;
265  document.body.append(a);
266  setTimeout(function () {
267    a.click();
268    a.remove();
269    setTimeout(() => URL.revokeObjectURL(a.href), 2000);
270  }, 100);
271};
272
273function get_fpl_main_data() {
274  return new Promise((resolve, reject) => {
275    $.ajax({
276      type: "GET",
277      url: `${proxy}/fantasy.premierleague.com/api/bootstrap-static/`,
278      dataType: "json",
279      async: true,
280      success: function (data) {
281        resolve(data);
282      },
283      error: function () {
284        console.log("Cannot get FPL main data");
285        reject("No data");
286      },
287    });
288  });
289}
290
291async function read_cached_static(req_season) {
292  return $.ajax({
293    type: "GET",
294    url: `data/${req_season}/static.json`,
295    async: true,
296    dataType: "json",
297    success: (data) => {
298      return data;
299    },
300    error: (xhr, status, error) => {
301      console.log(error);
302      console.error(xhr, status, error);
303    },
304  });
305}
306
307function get_entire_fixture() {
308  return new Promise((resolve, reject) => {
309    $.ajax({
310      type: "GET",
311      url: `${proxy}/fantasy.premierleague.com/api/fixtures/`,
312      dataType: "json",
313      async: true,
314      success: function (data) {
315        resolve(data);
316      },
317      error: function () {
318        console.log("Cannot get fixture");
319        reject("No data");
320      },
321    });
322  });
323}
324
325function get_fixture(gw) {
326  return new Promise((resolve, reject) => {
327    $.ajax({
328      type: "GET",
329      url: `${proxy}/fantasy.premierleague.com/api/fixtures/?event=${gw}`,
330      dataType: "json",
331      async: true,
332      success: function (data) {
333        let filtered_games = data.filter((i) => i.event == gw);
334        resolve(filtered_games);
335      },
336      error: function () {
337        console.log("Cannot get fixture");
338        reject("No data");
339      },
340    });
341  });
342}
343
344function get_sample_data(season, target_gw) {
345  let regular_sample = new Promise((resolve, reject) => {
346    $.ajax({
347      type: "GET",
348      url: `sample/${season}/${target_gw}/fpl_sampled.json`,
349      dataType: "json",
350      async: true,
351      success: function (data) {
352        resolve(data);
353      },
354      error: function () {
355        console.log("GW" + target_gw + " has no sample data");
356        reject("No data");
357      },
358    });
359  });
360
361  // let prime_sample = new Promise((resolve, reject) => {
362  //     $.ajax({
363  //         type: "GET",
364  //         url: `sample/${season}/${target_gw}/top_managers.json`,
365  //         dataType: "json",
366  //         async: true,
367  //         success: function(data) {
368  //             resolve(data);
369  //         },
370  //         error: function() {
371  //             console.log("GW" + target_gw + " has no sample data");
372  //             reject("No data");
373  //         }
374  //     });
375  // });
376
377  let promises = [regular_sample]; //, prime_sample]
378
379  return Promise.allSettled(promises);
380}
381
382// FPL awards real bonus once a fixture reaches finished_provisional, which is
383// hours before `finished` flips on the overnight update. Provisional bonus
384// derived from BPS must only be applied while no real bonus exists 
384yet,
385// otherwise it is added on top of the real thing and counted twice.
386function needsProvisionalBonus(game) {
387  if (!game.started || game.finished || game.finished_provisional) {
388    return false;
389  }
390  var awarded = game.stats.find(function (i) {
391    return i.identifier == "bonus";
392  });
393  if (awarded && (awarded.h.length > 0 || awarded.a.length > 0)) {
394    return false;
395  }
396  return true;
397}
398
399function get_cached_element_data({ season, gw, date }) {
400  return new Promise((resolve, reject) => {
401    $.ajax({
402      type: "GET",
403      url: `data/${season}/${gw}/${date}/input/element.csv`,
404      dataType: "text",
405      async: true,
406      success: function (data) {
407        // Proper CSV parsing: element.csv has quoted fields containing
408        // commas (price_change_projections), which a naive split breaks.
409        resolve($.csv.toObjects(data));
410      },
411      error: function (xhr, status, error) {
412        console.log(error);
413        console.error(xhr, status, error);
414        reject("No data");
415      },
416    });
417  });
418}
419
420function get_team_info(team_id) {
421  return new Promise((resolve, reject) => {
422    if (team_id == "-1") {
423      reject("Invalid team ID");
424    }
425    $.ajax({
426      type: "GET",
427      url: `${proxy}/fantasy.premierleague.com/api/entry/${team_id}/`,
428      async: true,
429      headers: {
430        "Access-Control-Allow-Origin": "*",
431        "Access-Control-Allow-Methods": "GET",
432        "X-Requested-With": "XMLHttpRequest",
433        "Referrer-Policy": "no-referrer-when-downgrade",
434      },
435      success: function (data) {
436        resolve(data);
437      },
438      error: function (xhr, status, error) {
439        reject("Cannot get team info");
440      },
441    });
442  });
443}
444
445function get_team_history(team_id) {
446  return new Promise((resolve, reject) => {
447    if (team_id == "-1") {
448      reject("Invalid team ID");
449    }
450    $.ajax({
451      type: "GET",
452      url: `${proxy}/fantasy.premierleague.com/api/entry/${team_id}/history/`,
453      dataType: "json",
454      async: true,
455      headers: {
456        "Access-Control-Allow-Origin": "*",
457        "Access-Control-Allow-Methods": "GET",
458        "X-Requested-With": "XMLHttpRequest",
459        "Referrer-Policy": "no-referrer-when-downgrade",
460      },
461      success: function (data) {
462        resolve(data);
463      },
464      error: function (xhr, status, error) {
465        reject("Cannot get team info");
466      },
467    });
468  });
469}
470
471function get_team_picks({ gw, team_id, force_last_gw }) {
472  return new Promise((resolve, reject) => {
473    if (team_id == "-1") {
474      reject("Team ID not valid");
475    }
476    $.ajax({
477      type: "GET",
478      url: `${proxy}/fantasy.premierleague.com/api/entry/${team_id}/event/${gw}/picks/`,
479      dataType: "json",
480      async: true,
481      headers: {
482        "Access-Control-Allow-Origin": "*",
483        "Access-Control-Allow-Headers": "X-Requested-With",
484        "Access-Control-Allow-Methods": "GET",
485        "X-Requested-With": "XMLHttpRequest",
486        "Referrer-Policy": "no-referrer-when-downgrade",
487      },
488      success: function (data) {
489        resolve({ body: data, is_last_gw: false });
490      },
491      error: function (xhr, status, error) {
492        if (force_last_gw) {
493          let last_gw = "" + (parseInt(gw) - 1);
494
495          $.ajax({
496            type: "GET",
497            url: `${proxy}/fantasy.premierleague.com/api/entry/${team_id}/event/${last_gw}/picks/`,
498            dataType: "json",
499            async: true,
500            headers: {
501              "Access-Control-Allow-Origin": "*",
502              "Access-Control-Allow-Methods": "GET",
503              "X-Requested-With": "XMLHttpRequest",
504              "Referrer-Policy": "no-referrer-when-downgrade",
505            },
506            success: function (data) {
507              resolve({ body: data, is_last_gw: true });
508            },
509            error: function (xhr, status, error) {
510              reject("Cannot get picks for last GW");
511            },
512          });
513        } else {
514          reject("Cannot get picks for this GW");
515        }
516      },
517    });
518  });
519}
520
521function teamInfoDetails(team_info, field) {
522  try {
523    switch (field) {
524      case "last_gw_rank_overall":
525        return team_info.leagues.classic.find((i) => i.name == "Overall")
526          .entry_last_rank;
527      case "current_rank":
528        return team_info.leagues.classic.find((i) => i.name == "Overall")
529          .entry_rank;
530    }
531  } catch {
532    return "-";
533  }
534}
535
536function getXPData({ season, gw, date }) {
537  return new Promise((resolve, reject) => {
538    $.ajax({
539      type: "GET",
540      url: `data/${season}/${gw}/${date}/input/element_gameweek.csv`,
541      dataType: "text",
542      async: true,
543      success: (data) => {
544        let xp_data = $.csv.toObjects(data);
545        let filtered_data = xp_data.filter((i) => i.event == gw.slice(2));
546        resolve(filtered_data);
547      },
548      error: function (xhr, status, error) {
549        reject("Could not get XP data");
550      },
551    });
552  });
553}
554
555// Seasons where element_gameweek.csv is stored unencrypted
556const UNENCRYPTED_SEASONS = ["2020-21", "2025-26", "2026-27"];
557
558function getXPData_Fernet({ season, gw, date }) {
559  if (UNENCRYPTED_SEASONS.includes(season)) {
560    return getXPData({ season, gw, date });
561  }
562  return new Promise((resolve, reject) => {
563    $.ajax({
564      type: "GET",
565      url: `data/${season}/${gw}/${date}/input/element_gameweek.csv-encrypted`,
566      dataType: "text",
567      async: true,
568      success: (data) => {
569        var secret = new fernet.Secret(
570          "symZ1LvXcAtjllNsDmhSp-GT1gDNPXw0P5eijrU8ogQ="
571        );
572        var token = new fernet.Token({
573          secret: secret,
574          token: data,
575          ttl: 0,
576        });
577        let raw = token.decode();
578        let csvdata = $.csv.toObjects(raw);
579        resolve(csvdata.filter((i) => i.event == gw.slice(2)));
580      },
581      error: () => {
582        reject("Could not get XP data");
583      },
584    });
585  });
586}
587
588function getProjectionData({ season, gw, date }) {
589  return new Promise((resolve, reject) => {
590    let gwNum = parseInt(gw.replace("GW", ""));
591    $.ajax({
592      type: "GET",
593      url: `static/projection/gw${gwNum}.csv`,
594      dataType: "text",
595      async: true,
596      success: (data) => {
597        try {
598          let csvdata = $.csv.toObjects(data);
599          // Convert projection format to expected element_gameweek format
600          let convertedData = csvdata.map((row) => ({
601            player_id: parseInt(row.ID),
602            event: gwNum,
603            event_id: gwNum, // Using GW number as event_id for now
604            web_name: row.Name,
605            points_md: parseFloat(row[`${gwNum}_Pts`] || 0),
606            xmins_md: parseFloat(row[`${gwNum}_xMins`] || 0),
607            team: row.Team,
608            opp_team: "", // Not available in projection data
609          }));
610          resolve(convertedData);
611        } catch (error) {
612          console.error("Error parsing projection data:", error);
613          reject("Could not parse projection data");
614        }
615      },
616      error: () => {
617        reject("Could not get projection data");
618      },
619    });
620  });
621}
622
623function getDetailedData({ season, gw, date }) {
624  return new Promise((resolve, reject) => {
625    $.ajax({
626      type: "GET",
627      url: `data/${season}/${gw}/${date}/input/detailed-fplreview-free-planner.csv-encrypted`,
628      dataType: "text",
629      async: true,
630      success: (data) => {
631        var secret = new fernet.Secret(
632          "symZ1LvXcAtjllNsDmhSp-GT1gDNPXw0P5eijrU8ogQ="
633        );
634        var token = new fernet.Token({
635          secret: secret,
636          token: data,
637          ttl: 0,
638        });
639        let raw = token.decode();
640        let csvdata = $.csv.toObjects(raw);
641        resolve(csvdata);
642      },
643      error: () => {
644        reject("Could not get detailed data");
645      },
646    });
647  });
648}
649
650function getSeasonProjection({ season }) {
651  return new Promise((resolve, reject) => {
652    $.ajax({
653      type: "GET",
654      url: `data/${season}/season_projection.csv`,
655      dataType: "text",
656      async: true,
657      success: (data) => {
658        let csvdata = $.csv.toObjects(data);
659        resolve(csvdata);
660      },
661      error: () => {
662        reject("Could not get detailed data");
663      },
664    });
665  });
666}
667
668async function read_cached_xp(req_season) {
669  return $.ajax({
670    type: "GET",
671    url: `data/${req_season}/xp.csv`,
672    async: true,
673    dataType: "text",
674    success: (data) => {
675      return data;
676    },
677    error: (xhr, status, error) => {
678      console.log(error);
679      console.error(xhr, status, error);
680    },
681  });
682}
683
684function getRPData(gw) {
685  return new Promise((resolve, reject) => {
686    $.ajax({
687      type: "GET",
688      url: `${proxy}/fantasy.premierleague.com/api/event/${gw}/live/`,
689      dataType: "json",
690      async: true,
691      headers: {
692        "Access-Control-Allow-Origin": "*",
693        "Access-Control-Allow-Methods": "GET",
694        "X-Requested-With": "XMLHttpRequest",
695        "Referrer-Policy": "no-referrer-when-downgrade",
696      },
697      success: function (data) {
698        resolve(data.elements);
699      },
700      error: function (xhr, status, error) {
701        reject("Error when getting live RP data");
702      },
703    });
704  });
705}
706
707function getSeasonRPData(max_gw = 38) {
708  return new Promise((resolve, reject) => {
709    let calls = [];
710    for (gw = 1; gw <= max_gw; gw++) {
711      let current_gw = gw;
712      let call = getRPData(gw)
713        .then((data) => {
714          let d = {
715            gw: current_gw,
716            data: data
717              .filter((i) => i.stats.minutes != 0)
718              .map((i) => {
719                return { id: i.id, e: i.explain };
720              }),
721          };
722          return d;
723        })
724        .catch((e) => {
725          // ignore
726        });
727      calls.push(call);
728    }
729
730    Promise.allSettled(calls)
731      .then((data) => {
732        let combined = data
733          .filter((i) => i.status == "fulfilled")
734          .map((i) => i.value)
735          .map((i) => [i.gw, i.data]);
736        let all_rp = Object.fromEntries(combined);
737        resolve(all_rp);
738      })
739      .catch((e) => {
740        reject(e);
741      });
742  });
743}
744
745function read_local_file(url) {
746  return new Promise((resolve, reject) => {
747    $.ajax({
748      type: "GET",
749      url: url,
750      async: true,
751      success: function (data) {
752        resolve(data);
753      },
754      error: function () {
755        reject("No data");
756      },
757    });
758  });
759}
760
761function read_local_file_Fernet(url) {
762  return new Promise((resolve, reject) => {
763    $.ajax({
764      type: "GET",
765      url: url,
766      dataType: "text",
767      async: true,
768      success: (data) => {
769        var secret = new fernet.Secret(
770          "symZ1LvXcAtjllNsDmhSp-GT1gDNPXw0P5eijrU8ogQ="
771        );
772        var token = new fernet.Token({
773          secret: secret,
774          token: data,
775          ttl: 0,
776        });
777        let raw = token.decode();
778        resolve(raw);
779      },
780      error: () => {
781        reject("Could not get XP data");
782      },
783    });
784  });
785}
786
787async function read_cached_rp(req_season) {
788  return $.ajax({
789    type: "GET",
790    url: `data/${req_season}/points.json?ts=${ts}`,
791    async: true,
792    dataType: "json",
793    success: (data) => {
794      return data;
795    },
796    error: (xhr, status, error) => {
797      console.log(error);
798      console.error(xhr, status, error);
799    },
800  });
801}
802
803function get_analytics_data({ season, gw }) {
804  return new Promise((resolve, reject) => {
805    $.ajax({
806      type: "GET",
807      url: `sample/${season}/${gw}/analytics_league.json`,
808      dataType: "json",
809      async: true,
810      success: (data) => {
811        resolve(data);
812      },
813      error: () => {
814        reject("Could not find league data");
815      },
816    });
817  });
818}
819
820let rank_formatter = Intl.NumberFormat("en");
821
822function formatted_rank(value) {
823  return rank_formatter.format(value);
824}
825
826function modify_time(origin, hour) {
827  let t0 = new Date(origin);
828  t0.setHours(t0.getHours() + hour);
829  return t0.getTime();
830}
831
832function getScreenCoords(x, y, ctm) {
833  var xn = ctm.e + x * ctm.a + y * ctm.c;
834  var yn = ctm.f + x * ctm.b + y * ctm.d;
835  return { x: xn, y: yn };
836}
837
838function convertToJS(value) {
839  if (value == "true") {
840    return true;
841  } else if (value == "false") {
842    return false;
843  } else {
844    return value;
845  }
846}
847
848function millisecondsToStr(milliseconds) {
849  let temp = milliseconds / 1000;
850  const days = Math.floor((temp %= 31536000) / 86400),
851    hours = Math.floor((temp %= 86400) / 3600),
852    minutes = Math.floor((temp %= 3600) / 60),
853    seconds = temp % 60;
854
855  if (days || hours || seconds || minutes) {
856    return (
857      (days ? days + " days " : "") +
858      (hours ? hours + " hours " : "") +
859      (minutes ? minutes + " minutes " : "") +
860      Number.parseFloat(seconds).toFixed(0) +
861      " seconds"
862    );
863  }
864
865  return "< 1 second";
866}
867
868function exportTableToCSV(selector, filename) {
869  var csv = [];
870  var rows = document.querySelectorAll(selector + " tr");
871
872  for (var i = 0; i < rows.length; i++) {
873    var row = [],
874      cols = rows[i].querySelectorAll("td, th");
875    for (var j = 0; j < cols.length; j++) row.push(cols[j].innerText);
876    csv.push(row.join(","));
877  }
878  downloadCSV(csv.join("\n"), filename);
879}
880
881function downloadCSV(csv, filename) {
882  var csvFile;
883  var downloadLink;
884
885  csvFile = new Blob([csv], { type: "text/csv" });
886
887  downloadLink = document.createElement("a");
888  downloadLink.download = filename;
889  downloadLink.href = window.URL.createObjectURL(csvFile);
890  downloadLink.style.display = "none";
891  document.body.appendChild(downloadLink);
892  downloadLink.click();
893  setTimeout(() => {
894    downloadLink.remove();
895  }, 500);
896}
897
898function saveAs(uri, filename) {
899  var link = document.createElement("a");
900  if (typeof link.download === "string") {
901    link.href = uri;
902    link.download = filename;
903    //Firefox requires the link to be in the body
904    document.body.appendChild(link);
905    //simulate click
906    link.click();
907    //remove the link when done
908    document.body.removeChild(link);
909  } else {
910    window.open(uri);
911  }
912}
913
914function createTeamFromList(
915  sorted,
916  picks,
917  cap,
918  vice_cap,
919  tc,
920  el_dict,
921  xp_data
922) {
923  if (sorted) {
924    let team = {};
925    team["picks"] = [];
926    picks.forEach((v, i) => {
927      team.picks.push({
928        element: v,
929        multiplier: v == cap ? (tc == 1 ? 3 : 2) : i <= 10 ? 1 : 0,
930        is_captain: v == cap,
931        is_vice_captain: v == vice_cap,
932        position: i + 1,
933      });
934    });
935
936    let have_cap = team.picks.find((i) => i.is_captain);
937    if (have_cap == undefined) {
938      let e = team.picks.find((i) => !i.is_vice_captain && i.multiplier > 0);
939      e.is_captain = true;
940      e.multiplier = 2;
941    }
942
943    let have_vcap = team.picks.find((i) => i.is_vice_captain);
944    if (have_vcap == undefined) {
945      let e = team.picks.find((i) => !i.is_captain && i.multiplier > 0);
946      e.is_vice_captain = true;
947    }
948
949    return _.cloneDeep(team);
950  } else {
951    let team = {};
952    team["picks"] = [];
953    // id, element_type, name, xp, xmin_threshold, xp_if_plays
954    let picks_with_xp = picks.map((i) => [
955      i,
956      el_dict[i].element_type,
957      el_dict[i].web_name,
958      parseFloat((xp_data[i] && xp_data[i][0]) || 0),
959      parseFloat((xp_data[i] && xp_data[i][1]) || 0),
960      parseFloat((xp_data[i] && xp_data[i][2]) || 0),
961    ]);
962    let ordered_picks = _.orderBy(picks_with_xp, ["4", "5"], ["desc", "desc"]);
963    // 1) sort by if played over 60 + points per scenario
964    let position_bounds = {
965      // 1: {'min': 1, 'max': 1},
966      2: { min: 3, max: 5 },
967      3: { min: 2, max: 5 },
968      4: { min: 1, max: 3 },
969    };
970
971    let main_gk;
972    let bench_gk;
973    let lineup = [];
974    let bench = [];
975
976    main_gk = ordered_picks.filter((i) => i[1] == 1)[0][0];
977    bench_gk = ordered_picks.filter((i) => i[1] == 1)[1][0];
978
979    for (let pos in position_bounds) {
980      if (pos == 1) {
981        continue;
982      }
983      let picked = ordered_picks
984        .filter((i) => i[1] == pos)
985        .slice(0, position_bounds[pos].min);
986      picked.forEach((i) => lineup.push(i[0]));
987    }
988    // 2) then use averages for remaining players
989    let remaining = _.orderBy(
990      ordered_picks.filter((i) => !lineup.includes(i[0]) && i[1] != 1),
991      "3",
992      "desc"
993    );
994    for (let i = lineup.length; i < 10; i++) {
995      lineup.push(remaining.shift()[0]);
996    }
997    bench = remaining.map((i) => i[0]);
998
999    cap = ordered_picks[0][0];
1000    vice_cap =
1001      ordered_picks[0][1] == 1 && ordered_picks[1][1] == 1
1002        ? ordered_picks[2][0]
1003        : ordered_picks[1][0];
1004    let squad = [main_gk].concat(lineup).concat([bench_gk]).concat(bench);
1005
1006    squad.forEach((v, i) => {
1007      team.picks.push({
1008        element: v,
1009        multiplier: v == cap ? 2 : i <= 10 ? 1 : 0,
1010        is_captain: v == cap,
1011        is_vice_captain: v == vice_cap,
1012        position: i + 1,
1013      });
1014    });
1015
1016    return _.cloneDeep(team);
1017  }
1018}
1019
1020function read_xp_storage() {
1021  let raw_data = window.localStorage.getItem("xp_storage");
1022  if (raw_data) {
1023    let json_data = JSON.parse(raw_data);
1024    return json_data;
1025  }
1026  return undefined;
1027}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.