PageSourceSearch

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

js fploptimized.com collected 2026-10-04 13:32:10 UTC 103,734 bytes, 3,483 lines download raw bytes

1// non-reactive values
2autosub_stats = {};
3
4let current_season = "2026_2027";
5
6Vue.config.devtools = true;
7Vue.config.productionTip = false;
8Vue.config.silent = false;
9Vue.config.errorHandler = (err, vm, info) => {
10  console.error("[Vue error]", info, err);
11  debugger;
12};
13Vue.config.warnHandler = (msg, vm, trace) => {
14  console.warn("[Vue warn]", msg, trace);
15};
16window.onerror = (m, s, l, c, e) => {
17  console.error("[window.onerror]", m, s + ":" + l);
18  debugger;
19};
20window.addEventListener("unhandledrejection", (e) => {
21  console.error("[unhandledrejection]", e.reason);
22  debugger;
23});
24
25var app = new Vue({
26  el: "#app",
27  //   render(h) {
28  //     return h("div", "Vue mounted OK");
29  //   },
30  data: {
31    cnt: 0,
32    season: season,
33    gw: gw,
34    next_gw: next_gw,
35    date: date,
36    now_dt: undefined,
37    listdates: listdates,
38    is_active: is_active,
39    active_gw: active_gw,
40    gw_fixture: undefined,
41    team_id: "-1",
42    current_team_id: "",
43    remember_settings: false,
44    allowed_settings: [
45      "team_id",
46      "fill_width",
47      "large_graphs",
48      "show_details",
49      "show_team_info",
50      "is_using_hits",
51      "is_using_autosub",
52      "ownership_source",
53      "no_white_space",
54    ],
55    team_info: undefined,
56    using_last_gw_team: false,
57    team_data: undefined,
58    original_team_data: undefined,
59    el_data: undefined,
60    xp_data: undefined,
61    xp_source: "Solio",
62    rp_data: undefined,
63    original_xp: undefined,
64    xp_storage: undefined,
65    ownership_source: "Official FPL API",
66    available_sources: ["Official FPL API"],
67    sample_data: undefined,
68    // selected_game: undefined
69    modal_selected_game: undefined,
70    game_table: undefined,
71    target_player: undefined,
72    is_using_hits: true,
73    is_using_autosub: true,
74    show_team_info: false,
75    fill_width: false,
76    large_graphs: false,
77    // autosub_stats: {},
78    marked_dt: undefined,
79    show_details: true,
80    show_league: false,
81    remember_me_button: false,
82    // sortable tables
83    sorted_ownership_cache: [],
84    sorted_ownership_cache_last_sort: undefined,
85    selected_player: undefined,
86    last_gw_data_marker: false,
87    no_white_space: false,
88    override_rp: [],
89    dropdown_select: 0,
90    use_upload_data: true,
91  },
92  beforeMount: function () {
93    this.initEmptyData();
94  },
95  computed: {
96    valid_team_id() {
97      return this.team_id == -1 || this.team_id == -2
98        ? "Click to enter"
99        : this.show_team_info
100        ? this.team_id
101        : "Hidden";
102    },
103    is_all_ready() {
104      return (
105        this.is_xp_ready &&
106        this.is_fixture_ready &&
107        this.is_el_ready &&
108        this.is_ownership_ready
109      );
110    },
111    is_ready() {
112      return this.team_id == -1 ||
113        this.team_data == undefined ||
114        this.team_data.length == 0
115        ? false
116        : true;
117    },
118    is_team_info_ready() {
119      return this.team_info !== undefined && !_.isEmpty(this.team_info);
120    },
121    is_rp_ready() {
122      return this.rp_data !== undefined && this.rp_data.length != 0;
123    },
124    is_xp_ready() {
125      return this.xp_data !== undefined && this.xp_data.length != 0;
126    },
127    is_fixture_ready() {
128      return this.gw_fixture !== undefined && this.gw_fixture.length != 0;
129    },
130    is_el_ready() {
131      return this.el_data !== undefined && this.el_data.length != 0;
132    },
133    is_ownership_ready() {
134      return (
135        this.ownership_data !== undefined && this.ownership_data.length != 0
136      );
137    },
138    seasongwdate: {
139      get: function () {
140        return this.season + " / " + this.gw + " / " + this.date;
141      },
142      set: function (value) {
143        let v = value.split(" / ");
144        this.season = v[0];
145        this.gw = v[1];
146        this.date = v[2];
147        this.$nextTick(() => {
148          app_initialize(this.team_id != -2);
149        });
150      },
151    },
152    ownership_source_buffer: {
153      get: function () {
154        return this.ownership_source;
155      },
156      set: function (value) {
157        setTimeout(() => {
158          app.ownership_source = value;
159          app.$nextTick(() => {
160            refresh_all_graphs();
161          });
162        }, 50);
163      },
164    },
165    is_using_sample() {
166      return (
167        this.ownership_source !== "Official FPL API" &&
168        !_.isEmpty(this.sample_data)
169      );
170    },
171    problem_with_sample_exist() {
172      if (
173        this.ownership_source !== "Official FPL API" &&
174        _.isEmpty(this.sample_data)
175      ) {
176        this.ownership_source = "Official FPL API";
177      }
178    },
179    current_sample_data() {
180      if (!this.is_using_sample) {
181        return [];
182      }
183      let key = reverse_sample_name(this.ownership_source);
184      if (key in this.sample_data) {
185        return this.sample_data[key];
186      } else {
187        return this.sample_data["Overall"];
188      }
189    },
190    ownership_data() {
191      if (!this.is_using_autosub || !this.is_using_sample) {
192        let own_data = get_ownership_by_type(
193          this.ownership_source,
194          this.el_data,
195          this.sample_data,
196          {}
197        );
198        autosub_stats["sub"] = [];
199        autosub_stats["cap"] = [];
200        return own_data.data;
201      } else {
202        let el_data = _.cloneDeep(this.el_data);
203        let autosubs = [];
204        let rp_by_id = this.rp_by_id;
205
206        el_data.forEach((e) => {
207          autosubs.push([
208            e.id,
209            {
210              element_type: e.element_type,
211              autosub:
212                rp_by_id && rp_by_id[e.id] ? rp_by_id[e.id].autosub : false,
213            },
214          ]);
215        });
216        // let autosubs = Object.fromEntries(Object.values(rp_by_id).map(i => [i.id, { autosub: i.autosub, element_type: i.element_type }]))
217        let autosub_dict = Object.fromEntries(autosubs);
218        let own_data = get_ownership_by_type(
219          this.ownership_source,
220          el_data,
221          this.sample_data,
222          autosub_dict
223        );
224        autosub_stats["sub"] = own_data.sub_replacement;
225        autosub_stats["cap"] = own_data.cap_replacement;
226        return own_data.data;
227      }
228    },
229    ownership_by_id() {
230      const ownership = this.ownership_data;
231      if (ownership == undefined) {
232        return {};
233      }
234      let own_object = Object.fromEntries(ownership.map((i) => [i.id, i]));
235      return own_object;
236    },
237    sorted_ownership() {
238      if (_.isEmpty(this.element_data_combined)) {
239        this.sorted_ownership_cache = [];
240        return [];
241      }
242      const all_elements = Object.values(this.element_data_combined);
243      let sorted_own = _.orderBy(
244        all_elements,
245        ["ownership", "xp"],
246        ["desc", "desc"]
247      );
248      this.sorted_ownership_cache = Object.freeze(sorted_own);
249      return 0;
250    },
251    team_eo_defense() {
252      return undefined;
253
254      if (_.isEmpty(this.ownership_data) || !this.is_ready) {
255        return [];
256      }
257      let data = this.gameweek_games_with_metadata;
258      if (_.isEmpty(data)) {
259        return [];
260      }
261
262      let entries = [];
263      data.forEach((game) => {
264        let teams = [
265          teams_ordered[game["team_h"] - 1],
266          teams_ordered[game["team_a"] - 1],
267        ];
268        teams.forEach((team, order) => {
269          let players = order == 0 ? game.players.home : game.player.away;
270          let defenders = players.filter((i) => i.data.element_type <= 2);
271          let field_eo = _.sum(defenders.map((i) => i.ownership));
272
273          let your_eo = _.sum(defenders.map((i) => i.multiplier)) / 100;
274          let net_eo = your_eo - field_eo;
275          let cs = game.started ? 1 : "-";
276          // TODO haven't finished yet
277        });
278      });
279    },
280    el_by_id() {
281      if (this.el_data == undefined) {
282        return undefined;
283      }
284      return Object.fromEntries(this.el_data.map((i) => [i.id, i]));
285    },
286    xp_by_id() {
287      if (this.xp_data == undefined) {
288        return undefined;
289      }
290      return Object.fromEntries(this.xp_data.map((i) => [i.player_id, i]));
291    },
292    rp_by_id() {
293      if (_.isEmpty(this.rp_data)) {
294        if (_.isEmpty(this.xp_data)) {
295          return undefined;
296        } else {
297          let ids = this.xp_data.map((i) => [i.player_id, { rp: 0 }]);
298          return Object.fromEntries(ids);
299        }
300      }
301      let rp_original = _.cloneDeep(this.rp_data);
302      let override = this.override_rp;
303      const fixture = this.gw_fixture;
304      // Autosub
305      rp_original.forEach((p) => {
306        let total_score = getSum(
307          p.explain.map((i) => i.stats.map((j) => j.points)).flat()
308        );
309        p.rp = total_score;
310        p.rp_original = total_score;
311        try {
312          p.games_finished = p.explain
313            .map(
314              (i) => fixture.find((j) => j.id == i.fixture).finished_provisional
315            )
316            .every((i) => i);
317          const p_type = app.el_by_id[p.id].element_type;
318          if (p.games_finished && p.stats.minutes == 0 && p_type != 5) {
319            p.autosub = true;
320          } else {
321            p.autosub = false;
322          }
323        } catch (e) {
324          console.log("Player game_finished error", e);
325        }
326      });
327      let rp_obj = Object.fromEntries(
328        _.cloneDeep(rp_original.map((i) => [i.id, i]))
329      );
330      if (!_.isEmpty(this.provisional_bonus) && !_.isEmpty(rp_obj)) {
331        Object.entries(this.provisional_bonus).forEach((entry) => {
332          const [key, value] = entry;
333          rp_obj[key].stats.total_points += value;
334        });
335      }
336      // override
337      override.forEach((e) => {
338        let b = _.cloneDeep(rp_obj[e.id]);
339        b.rp = e.rp;
340        b.explain = [
341          {
342            fixture: b.explain[0].fixture,
343            stats: [{ identifier: "override", points: e.rp, value: 0 }],
344          },
345        ];
346        rp_obj[e.id] = b;
347      });
348      return rp_obj;
349    },
350    xp_storage_this_gw() {
351      if (_.isEmpty(this.xp_storage)) {
352        return false;
353      }
354      let this_gw = parseInt(this.gw.replace("GW", ""));
355      return !_.isEmpty(
356        this.xp_storage.find(
357          (i) => i.meta.start_gw == this_gw && i.meta.status == "active"
358        )
359      );
360    },
361    gameweek_info() {
362      if (this.gw_fixture.length == 0) {
363        return {};
364      }
365      let fixture = this.gw_fixture;
366      let gw_info = {};
367      gw_info.start_dt = fixture[0].start_dt;
368      gw_info.end_dt = fixture[fixture.length - 1].end_dt;
369      gw_info.channels = Math.max(...fixture.map((i) => i.order));
370      return gw_info;
371    },
372    grouped_xp_data() {
373      let pts = _.cloneDeep(this.xp_data);
374      let pts_grouped = _(pts)
375        .groupBy("player_id")
376        .values()
377        .map((group) => ({
378          ...group[0],
379          qty: group.length,
380          event_list: group.map((k) => parseInt(k.event_id)),
381        }))
382        .value();
383      return pts_grouped;
384    },
385    gameweek_games_with_metadata() {
386      if (!this.is_fixture_ready) {
387        return [];
388      }
389      if (!this.is_xp_ready) {
390        return [];
391      }
392      if (!this.is_ownership_ready) {
393        return [];
394      }
395
396      const xp_data = this.grouped_xp_data;
397      const xp_by_id = Object.fromEntries(xp_data.map((i) => [i.player_id, i]));
398      const rp_by_id = this.rp_by_id;
399      let el_by_id = this.el_by_id;
400      const team_data = this.team_data;
401      const el_complete = this.element_data_combined;
402      const rp_ready = this.is_rp_ready;
403
404      const fixture_data = this.gw_fixture;
405      const ownership_vals = this.ownership_by_id;
406
407      let picks;
408      if (this.is_ready) {
409        picks = team_data.picks;
410      } else {
411        picks = [];
412      }
413      let picks_by_id = Object.fromEntries(picks.map((i) => [i.element, i]));
414      let cloned_fixture = _.cloneDeep(fixture_data);
415
416      // TODO: Assign expected totals and realized totals to every game
417      cloned_fixture.forEach((game) => {
418        game.game_string = this.game_label(game);
419        let players_in_this_game = xp_data
420          .filter((i) => i.event_list.includes(game.id))
421          .map((i) => i.player_id);
422
423        game.player_list = players_in_this_game;
424        let player_with_data = players_in_this_game.map((i) =>
425          Object.fromEntries([["id", parseInt(i)]])
426        );
427
428        let bps_provisional = {};
429
430        if (needsProvisionalBonus(game)) {
431          try {
432            let bps_stats = _.cloneDeep(
433              game.stats.find((i) => i.identifier == "bps")
434            );
435            let home_players = bps_stats.h.map((i) => {
436              i["home"] = true;
437              return i;
438            });
439            let away_players = bps_stats.a.map((i) => {
440              i["home"] = false;
441              return i;
442            });
443            let all_players = home_players.concat(away_players);
444            let sorted_groups = Object.entries(
445              _.groupBy(all_players, (i) => i.value)
446            )
447              .sort((a, b) => b[0] - a[0])
448              .slice(0, 3);
449            let count = 3;
450            let bonus = 3;
451            sorted_groups.forEach((cat, i) => {
452              if (count <= 0) {
453                cat.push(false);
454                return;
455              }
456              cat[1].forEach((p) => {
457                p.raw = p.value;
458                p.value = bonus;
459                bps_provisional[p.element] = bonus;
460              });
461              count -= cat[1].length;
462              bonus -= cat[1].length;
463              cat.push(true);
464            });
465            let final_list = sorted_groups
466              .filter((i) => i[2])
467              .map((i) => i[1])
468              .flat();
469            let prov_bps_stats = {
470              identifier: "bps_provisional",
471              a: final_list.filter((i) => !i.home),
472              h: final_list.filter((i) => i.home),
473            };
474            game.stats.push(prov_bps_stats);
475          } catch (err) {}
476        }
477
478        player_with_data.forEach((e) => {
479          let player_xp = xp_by_id[e.id];
480          e.xp = player_xp.points_md / Math.max(player_xp.event_list.length, 1);
481          e.data = el_complete[e.id];
482
483          if (rp_ready) {
484            let find = rp_by_id[e.id].explain.find((i) => i.fixture == game.id);
485            if (find == undefined) {
486              e.rp_detail = [];
487              e.rp = 0;
488            } else {
489              e.rp_detail = rp_by_id[e.id].explain
490                .find((i) => i.fixture == game.id)
491                .stats.map((i) => [i.identifier, i.points, i.value]);
492              e.rp = getSum(e.rp_detail.map((i) => i[1]));
493              if (e.id in bps_provisional) {
494                let bonus = bps_provisional[e.id];
495                e.rp += bonus;
496                e.rp_detail.push(["bps_provisional", bonus, 1]);
497              }
498            }
499          } else {
500            e.rp_detail = [];
501            e.rp = 0;
502          }
503
504          let player_match = picks_by_id[e.id];
505          e.ownership = this.is_using_sample
506            ? ((ownership_vals[e.id] &&
507                ownership_vals[e.id].effective_ownership) ||
508                0) / 100
509            : ((ownership_vals[e.id] &&
510                ownership_vals[e.id].selected_by_percent) ||
511                0) / 100;
512          e.multiplier = player_match ? player_match.multiplier : 0;
513          e.xp_net = (e.multiplier - e.ownership) * e.xp;
514          e.rp_net = (e.multiplier - e.ownership) * e.rp;
515          e.eo_owned = e.multiplier - e.ownership;
516          e.eo_nonowned = e.ownership;
517        });
518        game.player_details = player_with_data;
519        let split_players = _.groupBy(
520          game.player_details,
521          (e) => e.multiplier > 0
522        );
523        let owned_players = split_players[true] || [];
524        let nonowned_players = split_players[false] || [];
525
526        game.xp_sum = getSum(game.player_details.map((i) => i.xp));
527        game.rp_sum = getSum(game.player_details.map((i) => i.rp));
528
529        game.xp_team_gain = getSum(owned_players.map((i) => i.xp_net));
530        game.xp_team_loss = getSum(nonowned_players.map((i) => i.xp_net));
531        game.rp_team_gain = getSum(owned_players.map((i) => i.rp_net));
532        game.rp_team_loss = getSum(nonowned_players.map((i) => i.rp_net));
533
534        game.players_owned = getSum(
535          game.player_details.map((i) => i.multiplier)
536        );
537        game.players_nonowned = nonowned_players.length;
538        game.players_owned_eo = getSum(owned_players.map((i) => i.eo_owned));
539        game.players_nonowned_eo = getSum(
540          nonowned_players.map((i) => i.eo_nonowned)
541        );
542
543        game.xp_team_net = game.xp_team_gain + game.xp_team_loss;
544        game.rp_team_net = game.rp_team_gain + game.rp_team_loss;
545        game.final_team_net = game.rp_team_net - game.xp_team_net;
546      });
547
548      cloned_fixture.forEach((game_info) => {
549        game_info.data = {};
550        for (let i of game_info.stats) {
551          let j = (game_info.data[i.identifier] = {});
552          // Def Con arrives as a running count for every player with at least
553          // one action, so keep only those who have reached the threshold for
554          // their position and banked the points.
555          let scored = (w) => {
556            if (i.identifier != "defensive_contribution") {
557              return true;
558            }
559            let el = el_by_id[w.element];
560            let target = el
561              ? defcon_thresholds[parseInt(el.element_type)]
562              : undefined;
563            return target !== undefined && w.value >= target;
564          };
565          j["home"] = i.h
566            .filter((w) => xp_by_id[w.element] !== undefined && scored(w))
567            .map(
568              (w) =>
569                xp_by_id[w.element].web_name +
570                (w.value > 1 ? ` (${w.value})` : "")
571            );
572          j["away"] = i.a
573            .filter((w) => xp_by_id[w.element] !== undefined && scored(w))
574            .map(
575              (w) =>
576                xp_by_id[w.element].web_name +
577                (w.value > 1 ? ` (${w.value})` : "")
578            );
579          j["total"] = j["home"].length + j["away"].length;
580        }
581        game_info.player_details.forEach((p) => {
582          p.web_name = xp_by_id[p.id].web_name;
583        });
584        game_info.player_details.sort((a, b) => {
585          return b.rp - a.rp || b.xp - a.xp;
586        });
587        game_info.players = _.groupBy(game_info.player_details, (i) =>
588          el_by_id[i.id].team == game_info.team_h ? "home" : "away"
589        );
590      });
591
592      return cloned_fixture;
593    },
594    player_counts() {
595      if (_.isEmpty(this.gameweek_games_with_metadata)) {
596        return {};
597      }
598
599      let game_details = this.gameweek_games_with_metadata;
600      let your_total = getSum(
601        game_details
602          .map((i) => i.player_details.map((i) => i.multiplier))
603          .flat()
604      );
605      let your_played = getSum(
606        game_details
607          .filter((i) => i.started)
608          .map((i) => i.player_details.map((i) => i.multiplier))
609          .flat()
610      );
611      let avg_total = getSum(
612        game_details.map((i) => i.player_details.map((i) => i.ownership)).flat()
613      );
614      let avg_played = getSum(
615        game_details
616          .filter((i) => i.started)
617          .map((i) => i.player_details.map((i) => i.ownership))
618          .flat()
619      );
620
621      return { your_total, your_played, avg_total, avg_played };
622    },
623    get_graph_checkpoints() {
624      if (this.gw_fixture.length == 0) {
625        return [];
626      }
627      if (_.isEmpty(this.team_data)) {
628        return [];
629      }
630
631      let intervals = [];
632      let interval_start = undefined;
633
634      const gw_info = this.gameweek_info;
635
636      let cloned_fixture = this.gameweek_games_with_metadata;
637
638      let all_discrete_events = cloned_fixture
639        .map((i) => [
640          { type: "start", dt: i.start_dt, game: i },
641          { type: "end", dt: i.end_dt, game: i },
642        ])
643        .flat();
644      all_discrete_events.push({
645        type: "now",
646        dt: new Date(app.now_dt),
647        game: undefined,
648      });
649      all_discrete_events.sort((a, b) => {
650        return a.dt - b.dt;
651      });
652      all_discrete_events = _.cloneDeep(all_discrete_events);
653
654      let now_values = {};
655
656      // Initial event
657      let team_checkpoints = [];
658      let initial_team = 0;
659      let initial_avg = 0;
660      if (this.is_using_hits) {
661        initial_team = -(
662          (this.team_data.entry_history &&
663            this.team_data.entry_history.event_transfers_cost) ||
664          0
665        );
666      }
667      if (this.is_using_hits && this.is_using_sample) {
668        let sample_d = this.current_sample_data;
669        let vals = sample_d.map(
670          (i) => i.data.entry_history.event_transfers_cost
671        );
672        vals = vals.map((i) => (i > 60 ? 0 : i));
673        initial_avg = -(getSum(vals) / sample_d.length);
674      }
675      let setoff = { team: initial_team, avg: initial_avg };
676      let current_status = {
677        time: modify_time(gw_info.start_dt.getTime(), -2),
678        expected: {
679          points: initial_team,
680          gain: initial_team,
681          loss: initial_avg,
682          diff: initial_team - initial_avg,
683        },
684        realized: {
685          points: initial_team,
686          gain: initial_team,
687          loss: initial_avg,
688          diff: initial_team - initial_avg,
689        },
690        average: { expected: initial_avg, realized: initial_avg },
691        projected: {
692          points: initial_team,
693          avg: initial_avg,
694          gain: initial_team,
695          loss: initial_avg,
696          diff: initial_team - initial_avg,
697        },
698        active_events: [],
699        finished_events: [],
700        discrete_order: 0,
701        reason: "init",
702      };
703      team_checkpoints.push(_.cloneDeep(current_status));
704
705      // for each ...
706      all_discrete_events.forEach((event) => {
707        let current_time = event.dt.getTime();
708        // let existing_entry = team_checkpoints.find(i => i.time == current_time)
709        let target_event = current_status;
710
711        target_event.discrete_order += 1;
712        target_event = current_status;
713
714        // target_event = current_status;
715        target_event.reason = event.type;
716        target_event.time = event.dt.getTime();
717        target_event.trigger = event;
718
719        if (event.type == "start") {
720          target_event.active_events.push(event);
721          if (interval_start == undefined) {
722            interval_start = target_event.time;
723          }
724        } else if (event.type == "end") {
725          target_event.active_events = target_event.active_events.filter(
726            (i) => i.game.id !== event.game.id
727          );
728          target_event.finished_events.push(event);
729          if (target_event.active_events.length == 0) {
730            intervals.push([interval_start, target_event.time]);
731            interval_start = undefined;
732          }
733        }
734        let values = this.get_points_for_time(
735          event.type,
736          current_time,
737          target_event.active_events,
738          target_event.finished_events,
739          setoff,
740          now_values
741        );
742        target_event.expected.points = values.xp_total;
743        target_event.realized.points = values.rp_total;
744        target_event.expected.gain = values.xp_gain;
745        target_event.realized.gain = values.rp_gain;
746        target_event.expected.loss = values.xp_loss;
747        target_event.realized.loss = values.rp_loss;
748        target_event.expected.diff = values.xp_diff;
749        target_event.realized.diff = values.rp_diff;
750        target_event.average.expected = values.avg_expected;
751        target_event.average.realized = values.avg_realized;
752
753        target_event.projected.points = values.proj_total;
754        target_event.projected.avg = values.proj_avg;
755        target_event.projected.gain = values.proj_gain;
756        target_event.projected.loss = values.proj_loss;
757        target_event.projected.diff = values.proj_diff;
758
759        if (event.type == "now") {
760          now_values = _.cloneDeep(values);
761        }
762
763        let existing_id = team_checkpoints.findIndex(
764          (i) => i.time == current_time
765        );
766        if (existing_id == -1) {
767          let entry = {
768            time: _.clone(target_event.time),
769            expected: _.clone(target_event.expected),
770            realized: _.clone(target_event.realized),
771            average: _.clone(target_event.average),
772            projected: _.clone(target_event.projected),
773            active_events: _.clone(target_event.active_events),
774            finished_events: _.clone(target_event.finished_events),
775            discrete_order: _.clone(target_event.discrete_order),
776            reason: _.clone(target_event.reason),
777          };
778          team_checkpoints.push(entry);
779        } else {
780          let entry = {
781            time: _.clone(target_event.time),
782            expected: _.clone(target_event.expected),
783            realized: _.clone(target_event.realized),
784            average: _.clone(target_event.average),
785            projected: _.clone(target_event.projected),
786            active_events: _.clone(target_event.active_events),
787            finished_events: _.clone(target_event.finished_events),
788            discrete_order: _.clone(target_event.discrete_order),
789            reason: _.clone(target_event.reason),
790          };
791          team_checkpoints[existing_id] = entry;
792        }
793      });
794
795      current_status.reason = "final";
796      current_status.time = modify_time(gw_info.end_dt.getTime(), 2);
797      current_status.discrete_order += 1;
798      team_checkpoints.push(_.cloneDeep(current_status));
799
800      console.log("intervals", intervals);
801
802      return { checkpoints: team_checkpoints, intervals: intervals };
803    },
804    selected_game_info() {
805      if (this.modal_selected_game == undefined) {
806        return undefined;
807      }
808      return this.gameweek_games_with_metadata[this.modal_selected_game];
809    },
810    element_data_combined() {
811      if (_.isEmpty(this.xp_data)) {
812        return {};
813      }
814
815      let all_ids = this.xp_data.map((i) => parseInt(i.player_id));
816      let new_element_data = {};
817
818      const xp_by_id = Object.fromEntries(
819        this.grouped_xp_data.map((i) => [i.player_id, i])
820      );
821      const el_by_id = this.el_by_id;
822      if (_.isEmpty(el_by_id)) {
823        return {};
824      }
825      const rp_by_id = this.rp_by_id;
826      const own_by_id = this.ownership_by_id;
827      const team_data = this.team_data;
828      let picks = [];
829      if (this.is_ready) {
830        picks = team_data.picks;
831      }
832      let team_ids = picks.map((i) => i.element);
833
834      all_ids.forEach((e) => {
835        try {
836          let n = {};
837          n.xp_data = xp_by_id[e];
838          n.el_data = el_by_id[e];
839          n.rp_data = (rp_by_id && rp_by_id[e]) || undefined;
840          n.autosub = false;
841          if (n.rp_data !== undefined && n.rp_data.autosub) {
842            n.autosub = true;
843          }
844          n.own_data = own_by_id[e];
845          n.name = n.el_data.web_name;
846          let points_md = (n.xp = parseFloat(n.xp_data.points_md));
847          let is_squad = (n.is_squad = team_ids.includes(e));
848          let multiplier = (n.multiplier = is_squad
849            ? picks.find((i) => i.element == e).multiplier
850            : 0);
851          n.is_lineup = multiplier > 0;
852          let ownership = (n.ownership = this.is_using_sample
853            ? n.own_data.effective_ownership
854            : parseFloat(n.own_data.selected_by_percent));
855
856          n.xp_gain = points_md * (Math.max(multiplier, 1) - ownership / 100);
857          n.xp_loss = (points_md * ownership) / 100;
858          n.xp_net = points_md * (multiplier - ownership / 100);
859
860          let rp = rp_by_id[e].rp;
861          n.rp_total = rp;
862          n.rp_gain = rp * (Math.max(multiplier, 1) - ownership / 100);
863          n.rp_loss = (rp * ownership) / 100;
864          n.rp_net = rp * (multiplier - ownership / 100);
865          n.rp_original = n.rp_data.rp_original || n.rp_data.rp;
866
867          n.luck = n.rp_net - n.xp_net;
868
869          n.element_type = parseInt(n.el_data.element_type);
870          n.team = team_codes[parseInt(n.el_data.team_code)];
871          n.now_cost_str = (parseFloat(n.el_data.now_cost) / 10).toFixed(1);
872          n.id = e;
873          new_element_data[e] = n;
874        } catch (err) {
875          console.log(err);
876          return;
877        }
878      });
879
880      return new_element_data;
881    },
882    element_data_list() {
883      if (!this.is_ready) {
884        return [];
885      }
886      if (!this.is_el_ready) {
887        return [];
888      }
889      return Object.values(this.element_data_combined);
890    },
891    other_elements() {
892      if (!this.is_ready) {
893        return {};
894      }
895      if (!this.is_el_ready) {
896        return {};
897      }
898      return new_element_data.filter((i) => i.is_squad == false);
899    },
900    team_data_with_metadata() {
901      if (!this.is_ready) {
902        return [];
903      }
904      if (!this.is_el_ready) {
905        return [];
906      }
907      if (_.isEmpty(this.team_data)) {
908        return [];
909      }
910
911      const el_data_combined = this.element_data_combined;
912      if (_.isEmpty(el_data_combined)) {
913        return [];
914      }
915      let picks = _.cloneDeep(this.team_data.picks);
916      let pos_ctr = { 1: 1, 2: 1, 3: 1, 4: 1, 5: 0, B: 1 };
917      picks.forEach((e) => {
918        e.data = el_data_combined[e.element];
919        let el_info = e.data.el_data;
920        Object.assign(e, e.data);
921      });
922
923      // If AM is among picks, move bench by 1
924      let has_am = 0;
925      if (picks.some((i) => i.element_type == 5)) {
926        pos_ctr["B"] += 1;
927        has_am = 1;
928      }
929
930      picks.forEach((player) => {
931        let data = player.data;
932        let cnt = picks
933          .filter((j) => j.element_type == data.element_type)
934          .filter((j) => j.multiplier > 0).length;
935        if (data.element_type == 5) {
936          // AM
937          player.x = (122 / (5 + has_am)) * 1 - 17;
938          player.y = 138.5;
939        } else if (data.multiplier > 0) {
940          player.x =
941            (122 / (cnt + 1)) * pos_ctr[parseInt(player.element_type)] - 17;
942          player.y = (parseInt(player.element_type) - 1) * 35 + 3;
943          pos_ctr[parseInt(player.element_type)] += 1;
944        } else {
945          player.x = (122 / (5 + has_am)) * pos_ctr["B"] - 17;
946          pos_ctr["B"] += 1;
947          player.y = 138.5;
948        }
949      });
950
951      picks.sort((a, b) => {
952        if (a.is_lineup !== b.is_lineup) {
953          return b.is_lineup - a.is_lineup;
954        } else if (a.element_type !== b.element_type) {
955          return parseInt(a.element_type) - parseInt(b.element_type);
956        } else {
957          return parseInt(a.element) - parseInt(b.element);
958        }
959      });
960
961      return picks;
962    },
963    potential_targets() {
964      if (this.target_player == undefined) {
965        return [];
966      }
967      let els = _.cloneDeep(this.element_data_list);
968      els = els.filter(
969        (i) => i.element_type == this.target_player.element_type
970      );
971      els.sort((a, b) => {
972        return b.xp - a.xp;
973      });
974      return els;
975    },
976    provisional_bonus() {
977      if (!this.is_fixture_ready) {
978        return {};
979      }
980
981      let bonus_players = {};
982
983      const games = this.gw_fixture;
984      games.forEach((game) => {
985        let bps_provisional = {};
986
987        if (needsProvisionalBonus(game)) {
988          try {
989            let bps_stats = game.stats.find((i) => i.identifier == "bps");
990            let all_players = bps_stats.h.concat(bps_stats.a);
991            let sorted_groups = Object.entries(
992              _.groupBy(all_players, (i) => i.value)
993            )
994              .sort((a, b) => b[0] - a[0])
995              .slice(0, 3);
996            sorted_groups.forEach((cat, i) => {
997              let bonus = 3 - i;
998              cat[1].forEach((p) => {
999                bonus_players[p.element] = bonus;
1000              });
1001            });
1002          } catch (err) {}
1003        }
1004      });
1005
1006      return bonus_players;
1007    },
1008    currentTeamFormation() {
1009      if (!this.is_ready) {
1010        return {};
1011      }
1012      let picks = this.team_data_with_metadata
1013        .filter((i) => i.multiplier > 0)
1014        .map((i) => i.element_type);
1015      const gk_count = picks.filter((i) => i == 1).length;
1016      const df_count = picks.filter((i) => i == 2).length;
1017      const md_count = picks.filter((i) => i == 3).length;
1018      const fw_count = picks.filter((i) => i == 4).length;
1019      let is_valid = true;
1020      if (
1021        gk_count !== 1 ||
1022        df_count < 3 ||
1023        md_count < 2 ||
1024        picks.length !== 11
1025      ) {
1026        is_valid = false;
1027      }
1028      const form_str = `(${gk_count}) ${df_count}-${md_count}-${fw_count}`;
1029      return {
1030        1: gk_count,
1031        2: df_count,
1032        3: md_count,
1033        4: fw_count,
1034        is_valid: is_valid,
1035        form_str: form_str,
1036      };
1037    },
1038    autosub_subs() {
1039      if (!this.is_using_autosub || !this.is_using_sample) {
1040        return [];
1041      }
1042      let el = this.element_data_combined;
1043      let rp = this.rp_by_id;
1044      let count = this.current_sample_data.length;
1045      let stats = autosub_stats.sub;
1046      let top_ones = Object.entries(_.countBy(stats))
1047        .sort((a, b) => b[1] - a[1])
1048        .slice(0, 15);
1049      const top_pairs = top_ones.map((s) => {
1050        let players = s[0].split(",");
1051        let change = 0;
1052        try {
1053          change = rp[players[1]].stats.total_points;
1054        } catch (e) {}
1055        return {
1056          occurence: s[1],
1057          ratio: s[1] / count,
1058          from: el[players[0]],
1059          to: el[players[1]],
1060          change: change,
1061          effect: getWithSign((-s[1] / count) * change),
1062        };
1063      });
1064      return top_pairs;
1065    },
1066    autosub_caps() {
1067      if (!this.is_using_autosub || !this.is_using_sample) {
1068        return [];
1069      }
1070      let el = this.element_data_combined;
1071      let rp = this.rp_by_id;
1072      let count = this.current_sample_data.length;
1073      let stats = autosub_stats.cap;
1074      let top_ones = Object.entries(_.countBy(stats))
1075        .sort((a, b) => b[1] - a[1])
1076        .slice(0, 15);
1077      const top_pairs = top_ones.map((s) => {
1078        let players = s[0].split(",");
1079        let change = 0;
1080        try {
1081          change = rp[players[1]].stats.total_points;
1082        } catch (e) {}
1083        return {
1084          occurence: s[1],
1085          ratio: s[1] / count,
1086          from: el[players[0]],
1087          to: el[players[1]],
1088          change: change,
1089          effect: getWithSign((-s[1] / count) * change),
1090        };
1091      });
1092      return top_pairs;
1093    },
1094    get_saved_settings() {
1095      let do_remember_settings = this.remember_settings;
1096      let saved_settings = {};
1097      let settings = this.allowed_settings;
1098      for (let st of settings) {
1099        let val = Vue.$cookies.get(current_season + "_" + st);
1100        if (val !== null) {
1101          saved_settings[st] = convertToJS(val);
1102        }
1103      }
1104      return saved_settings;
1105    },
1106    legendColors() {
1107      // Get theme-aware colors for consistency with graphs
1108      const isLightMode =
1109        document.documentElement.getAttribute("data-theme") === "light" ||
1110        document.body.getAttribute("data-theme") === "light";
1111
1112      if (isLightMode) {
1113        return {
1114          lineExpected: "#4CAF50", // Green
1115          lineRealized: "#2196F3", // Blue
1116          lineProjected: "#6366F1", // Indigo
1117          lineAverage: "#EC4899", // Pink
1118          lineYourTeam: "#F59E0B", // Amber
1119          lineFieldAvg: "#EF4444", // Red
1120        };
1121      } else {
1122        return {
1123          lineExpected: "#dbf7b4",
1124          lineRealized: "#6fcfd6",
1125          lineProjected: "#587ee0b5",
1126          lineAverage: "#e482baa8",
1127          lineYourTeam: "#ffc356",
1128          lineFieldAvg: "#de6363",
1129        };
1130      }
1131    },
1132  },
1133  watch: {
1134    // Watch for theme changes to trigger reactivity for legend colors
1135    legendColors: {
1136      handler() {
1137        // Force update to ensure legend colors are updated
1138        this.$forceUpdate();
1139      },
1140      deep: true,
1141    },
1142  },
1143  methods: {
1144    initEmptyData() {
1145      this.gw_fixture = [];
1146      this.team_data = [];
1147      this.el_data = [];
1148      this.xp_data = [];
1149      this.rp_data = [];
1150      this.sample_data = {};
1151      this.team_info = {};
1152      this.now_dt = new Date().getTime();
1153      this.modal_selected_game = undefined;
1154    },
1155    saveTeamData(data) {
1156      this.original_team_data = Object.freeze(_.cloneDeep(data));
1157      this.team_data = Object.freeze(data);
1158      if (this.is_using_autosub) {
1159        this.applyAutosubtoTeam();
1160      }
1161    },
1162    resetTeamData() {
1163      this.team_data = _.cloneDeep(this.original_team_data);
1164      refresh_all_graphs();
1165    },
1166    loadOptimal() {
1167      // $.ajax({
1168      //     type: "GET",
1169      //     url: `data/${this.season}/${this.gw}/${this.date}/output/limited_best_15_weighted.csv`,
1170      //     dataType: "text",
1171      //     async: true,
1172      //     success: (data) => {
1173      //         tablevals = data.split('\n').map(i => i.split(','));
1174      //         keys = tablevals[0];
1175      //         values = tablevals.slice(1);
1176      //         values_filtered = values.filter(i => i.length > 1);
1177      //         let squad = values_filtered.map(i => _.zipObject(keys, i));
1178      //         if(_.isEmpty(this.team_data)) {
1179      //             this.team_data = {'picks': [], 'event_transfers_cost': [], 'entry_history': {'event_transfers_cost': []}}
1180      //             squad.forEach((item, index) => {
1181      //                 item.element = parseInt(item.player_id)
1182      //                 item.multiplier = index < 11 ? (item.is_captain == "True" ? 2 : 1) : 0;
1183      //                 item.is_captain = item.is_captain == "True";
1184      //                 this.team_data.picks.push(item)
1185      //             })
1186      //             this.original_team_data = _.cloneDeep(this.team_data);
1187      //             this.$forceUpdate();
1188      //         }
1189      //         else {
1190      //             this.team_data.picks.forEach(function load(val, index) {
1191      //                 val.element = parseInt(squad[index].player_id);
1192      //                 val.multiplier = index < 11 ? (squad[index].is_captain == "True" ? 2 : 1) : 0;
1193      //                 val.is_captain = squad[index].is_captain == "True";
1194      //             })
1195      //         }
1196      //         this.$nextTick(() => {
1197      //             refresh_all_graphs();
1198      //         });
1199      //     },
1200      //     error: function(xhr, status, error) {
1201      //         console.log(xhr, status, error);
1202      //     }
1203      // });
1204    },
1205    saveTeamInfo(data) {
1206      this.team_info = Object.freeze(data);
1207    },
1208    saveEl(data) {
1209      this.el_data = Object.freeze(data);
1210    },
1211    saveXP(values) {
1212      this.xp_data = Object.freeze(values);
1213      if (this.xp_storage_this_gw) {
1214        this.useNewXP();
1215      } else {
1216        this.original_xp = undefined;
1217        this.xp_source = "Solio";
1218      }
1219    },
1220    useNewXP() {
1221      if (app.use_upload_data == false) {
1222        this.useOriginalXP();
1223        return;
1224      }
1225      let this_gw = parseInt(this.gw.replace("GW", ""));
1226      let xp_data = this.xp_storage;
1227      let feas = xp_data.find(
1228        (i) => i.meta.start_gw == this_gw && i.meta.status == "active"
1229      );
1230      if (feas) {
1231        let new_data = $.csv
1232          .toObjects(feas.data)
1233          .map((i, j) => [
1234            i.ID ? parseInt(i.ID) : i.id ? parseInt(i.id) : j + 1,
1235            i,
1236          ]);
1237        let new_data_dict = Object.fromEntries(new_data);
1238        let xp_existing = _.cloneDeep(this.xp_data);
1239        xp_existing.forEach((player) => {
1240          let new_entry = new_data_dict[player.player_id];
1241          if (new_entry) {
1242            player.points_md =
1243              new_entry[player.event + "_Pts"] || player.points_md;
1244            player.xmins_md =
1245              new_entry[player.event + "_xMins"] || player.xmins_md;
1246          }
1247        });
1248        this.original_xp = _.cloneDeep(this.xp_data);
1249        this.xp_data = Object.freeze(xp_existing);
1250        this.xp_source = feas.meta.filename;
1251        refresh_all_graphs();
1252      } else {
1253        this.useOriginalXP();
1254      }
1255    },
1256    useOriginalXP() {
1257      if (_.isEmpty(this.original_xp)) {
1258        return;
1259      }
1260      this.xp_source = "Solio";
1261      this.xp_data = Object.freeze(_.cloneDeep(this.original_xp));
1262      this.original_xp = undefined;
1263      refresh_all_graphs();
1264    },
1265    saveRP(values) {
1266      this.rp_data = Object.freeze(values);
1267    },
1268    saveFixtureData(data) {
1269      data.forEach((game, index) => {
1270        game.start_dt = new Date(game.kickoff_time);
1271        game.end_dt = new Date(game.start_dt.getTime() + 105 * 60 * 1000);
1272        game.node_info = {
1273          start: game.start_dt.getTime(),
1274          end: game.end_dt.getTime(),
1275          content: "Game",
1276        };
1277      });
1278
1279      data.sort((a, b) => {
1280        return a.node_info.start - b.node_info.start;
1281      });
1282
1283      data.forEach((game, index) => {
1284        game.duration = 105 * 60 * 1000;
1285        game.team_h_name = teams_ordered[game.team_h - 1].name;
1286        game.team_a_name = teams_ordered[game.team_a - 1].name;
1287        game.label =
1288          teams_ordered[game.team_h - 1].name +
1289          " vs " +
1290          teams_ordered[game.team_a - 1].name;
1291        let order = 0;
1292        data.slice(0, index).forEach((game2) => {
1293          if (
1294            (game.start_dt >= game2.start_dt &&
1295              game.start_dt <= game2.end_dt) ||
1296            (game.end_dt >= game2.start_dt && game.end_dt <= game2.end_dt)
1297          ) {
1298            if (game2.order == order) {
1299              order += 1;
1300            }
1301          }
1302        });
1303        game.order = order;
1304      });
1305      this.gw_fixture = Object.freeze(data);
1306
1307      let start_dt = data[0].start_dt;
1308      let end_dt = data[data.length - 1].end_dt;
1309
1310      if (new Date(this.now_dt) > end_dt) {
1311        this.now_dt = modify_time(end_dt.getTime(), 2);
1312      } else if (new Date(this.now_dt) < start_dt) {
1313        this.now_dt = modify_time(start_dt.getTime(), -2);
1314      }
1315    },
1316    saveSampleData(success, data) {
1317      if (success) {
1318        // clear missing teams
1319        keys = Object.keys(data);
1320        keys.forEach((k) => {
1321          data[k] = data[k].filter((i) => i.data != null);
1322        });
1323
1324        // fix for autosub
1325        autosubs = Object.values(data)
1326          .flat()
1327          .map((i) => i.data.automatic_subs)
1328          .flat();
1329        if (autosubs.length > 0) {
1330          app.is_using_autosub = false;
1331        } else {
1332          app.is_using_autosub = true;
1333        }
1334
1335        this.sample_data = Object.freeze(data);
1336        let sample_values = Object.keys(data)
1337          .reverse()
1338          .map((i) => "" + sample_compact_number(i));
1339        this.available_sources = ["Official FPL API"].concat(sample_values);
1340        if (this.ownership_source == this.available_sources[0]) {
1341          this.ownership_source = this.available_sources[2];
1342        }
1343      } else {
1344        this.sample_data = [];
1345        this.available_sources = ["Official FPL API"];
1346        this.ownership_source = this.available_sources[0];
1347      }
1348    },
1349    selectLeagueTeam() {
1350      this.current_team_id = $("#fpl_analytics_league_select").val();
1351    },
1352    // selectLeagueTeam() {
1353    //     $("#teamModal").modal('hide');
1354    //     $("#waitModal").modal({
1355    //         backdrop: 'static',
1356    //         keyboard: false
1357    //     }).modal('show');
1358    //     this.team_id = $("#fpl_analytics_league_select").val();
1359    //     if (this.team_id == "") { return; }
1360    //     this.$nextTick(() => {
1361    //         load_team_data(graph_refresh = true).then(() => {
1362    //             $("#waitModal").modal('hide');
1363    //         });
1364    //     })
1365    // },
1366    saveTeamId() {
1367      $("#teamModal").modal("hide");
1368      $("#waitModal")
1369        .modal({
1370          backdrop: "static",
1371          keyboard: false,
1372        })
1373        .modal("show");
1374      this.team_id = this.current_team_id; //$("#teamIdEnter").val();
1375      this.$nextTick(() => {
1376        $("#fpl_analytics_league_select").val("");
1377        load_team_data((graph_refresh = true)).then(() => {
1378          $("#waitModal").modal("hide");
1379        });
1380      });
1381      if (this.remember_me_button) {
1382        this.toggleRemember(true);
1383        this.remember_me_button = false;
1384      }
1385    },
1386    setTeamWithURL(sorted, picks, cap, vice_cap, tc, gw) {
1387      $("#waitModal")
1388        .modal({
1389          backdrop: "static",
1390          keyboard: false,
1391        })
1392        .modal("show");
1393
1394      this.team_id = -2;
1395
1396      let past_gw = false;
1397      if (gw) {
1398        for (const i in this.listdates) {
1399          let value = this.listdates[i];
1400          let this_gw = this.listdates[i].split("/")[1].split("GW")[1].trim();
1401          if (this_gw == gw) {
1402            if (gw != app.gw) {
1403              past_gw = true;
1404            }
1405            this.seasongwdate = value.trim();
1406          }
1407        }
1408      }
1409
1410      let pids = this.el_data.map((i) => i.id);
1411      let xp_data = Object.fromEntries(
1412        pids.map((i) => [
1413          i,
1414          [
1415            (this.xp_by_id[i] && this.xp_by_id[i].points_md) || 0,
1416            1,
1417            (this.xp_by_id[i] && this.xp_by_id[i].points_md) || 0,
1418          ],
1419        ])
1420      );
1421      data = createTeamFromList(
1422        sorted,
1423        picks,
1424        cap,
1425        vice_cap,
1426        tc,
1427        this.el_by_id,
1428        xp_data
1429      );
1430
1431      this.saveTeamData(data);
1432      if (past_gw) {
1433        if (this.is_using_autosub) {
1434          setTimeout(() => {
1435            this.applyAutosubtoTeam();
1436          }, 500);
1437        }
1438      }
1439
1440      this.$nextTick(() => {
1441        refresh_all_graphs();
1442        $("#waitModal").modal("hide");
1443      });
1444    },
1445    findIdealPicks() {
1446      if (_.isEmpty(this.team_data)) {
1447        return;
1448      }
1449      let pids = this.team_data.picks.map((i) => i.element);
1450      let xp_data = Object.fromEntries(
1451        pids.map((i) => [
1452          i,
1453          [
1454            (this.xp_by_id[i] && this.xp_by_id[i].points_md) || 0,
1455            1,
1456            (this.xp_by_id[i] && this.xp_by_id[i].points_md) || 0,
1457          ],
1458        ])
1459      );
1460      data = createTeamFromList(
1461        0,
1462        pids,
1463        undefined,
1464        undefined,
1465        undefined,
1466        this.el_by_id,
1467        xp_data
1468      );
1469      let d = _.cloneDeep(this.team_data);
1470      d.picks = data.picks;
1471      this.saveTeamData(d);
1472      setTimeout(() => {
1473        refresh_all_graphs();
1474      }, 500);
1475    },
1476    loadAutoSettings() {
1477      $("#waitModal")
1478        .modal({
1479          backdrop: "static",
1480          keyboard: false,
1481        })
1482        .modal("show");
1483      this.remember_settings = true;
1484      let settings = this.allowed_settings;
1485      for (let st of settings) {
1486        let val = Vue.$cookies.get(current_season + "_" + st);
1487        if (val !== null) {
1488          this[st] = convertToJS(val);
1489        }
1490      }
1491      this.$nextTick(() => {
1492        load_team_data((graph_refresh = true)).then(() => {
1493          $("#waitModal").modal("hide");
1494        });
1495      });
1496    },
1497    toggleRemember(force = null) {
1498      if (typeof force === "boolean") {
1499        this.remember_settings = !force;
1500      }
1501      if (this.remember_settings) {
1502        this.remember_settings = false;
1503        let settings = this.allowed_settings;
1504        for (let st of settings) {
1505          Vue.$cookies.remove(current_season + "_" + st);
1506        }
1507      } else {
1508        this.remember_settings = true;
1509        let settings = this.allowed_settings;
1510        for (let st of settings) {
1511          Vue.$cookies.set(current_season + "_" + st, this[st]);
1512        }
1513      }
1514    },
1515    game_label(game) {
1516      if (game == undefined) {
1517        return ".";
1518      }
1519      if (game.finished_provisional) {
1520        return (
1521          teams_ordered[game.team_h - 1].name +
1522          " vs " +
1523          teams_ordered[game.team_a - 1].name +
1524          " (" +
1525          game.team_h_score +
1526          "-" +
1527          game.team_a_score +
1528          ")"
1529        );
1530      } else if (game.started) {
1531        return (
1532          "(Live) " +
1533          teams_ordered[game.team_h - 1].name +
1534          " vs " +
1535          teams_ordered[game.team_a - 1].name +
1536          " (" +
1537          game.team_h_score +
1538          "-" +
1539          game.team_a_score +
1540          ")"
1541        );
1542      } else {
1543        return (
1544          teams_ordered[game.team_h - 1].name +
1545          " vs " +
1546          teams_ordered[game.team_a - 1].name
1547        );
1548      }
1549    },
1550    get_points_for_time(
1551      event_type,
1552      time,
1553      active_events,
1554      finished_events,
1555      setoff,
1556      now_values
1557    ) {
1558      const ownership_by_id = this.ownership_by_id;
1559
1560      let xp = 0;
1561      let picks = this.team_data.picks;
1562      if (!this.is_ready) {
1563        picks = [];
1564      }
1565      const team_ids = picks
1566        .filter((i) => i.multiplier > 0)
1567        .map((i) => i.element);
1568
1569      let finished_players = finished_events
1570        .map((i) => i.game.player_details)
1571        .flat();
1572      let finished_players_final = finished_players.map((i) => {
1573        let player_in_squad = picks.find((j) => j.element == i.id);
1574        let multiplier = 0;
1575        if (player_in_squad !== undefined) {
1576          multiplier = player_in_squad.multiplier;
1577        }
1578        let ow = ownership_by_id[i.id];
1579        let eo = ow.effective_ownership / 100;
1580        if (!this.is_using_sample) {
1581          eo = ow.selected_by_percent / 100;
1582        }
1583        return {
1584          id: i.id,
1585          player: i,
1586          multiplier: multiplier,
1587          xp: i.xp,
1588          rp: i.rp,
1589          eo: eo,
1590        };
1591      });
1592
1593      let split_players = _.groupBy(finished_players_final, (e) => {
1594        return team_ids.includes(e.id);
1595      });
1596
1597      // Part 1 - In my team
1598      let team_finished = split_players[true] || [];
1599      let xp_total = getSum(team_finished.map((i) => i.xp * i.multiplier));
1600      let rp_total = getSum(team_finished.map((i) => i.rp * i.multiplier));
1601      let xp_gain = getSum(
1602        team_finished.map((i) => i.xp * (i.multiplier - i.eo))
1603      );
1604      let rp_gain = getSum(
1605        team_finished.map((i) => i.rp * (i.multiplier - i.eo))
1606      );
1607
1608      // Part 2 - Not in my team
1609      let rest_finished = split_players[false] || [];
1610      let xp_loss = getSum(rest_finished.map((i) => i.xp * i.eo));
1611      let xp_diff = xp_gain - xp_loss;
1612      let rp_loss = getSum(rest_finished.map((i) => i.rp * i.eo));
1613      let rp_diff = rp_gain - rp_loss;
1614
1615      let average_expected = getSum(
1616        finished_players_final.map((i) => i.xp * i.eo)
1617      );
1618      let average_realized = getSum(
1619        finished_players_final.map((i) => i.rp * i.eo)
1620      );
1621
1622      // Now event update
1623      if (true) {
1624        let copy_active_events = _.cloneDeep(active_events);
1625
1626        copy_active_events.forEach((g) => {
1627          g.completed =
1628            (time - g.game.node_info.start) /
1629            Math.max(g.game.node_info.end - g.game.node_info.start, 1);
1630          g.game.player_details.forEach((el) => {
1631            el.xp = g.completed * el.xp;
1632            if (event_type !== "now") {
1633              el.rp = g.completed * el.rp;
1634            }
1635          });
1636        });
1637
1638        let active_players = copy_active_events
1639          .map((i) => i.game.player_details)
1640          .flat();
1641        let active_players_final = active_players.map((i) => {
1642          let player_in_squad = picks.find((j) => j.element == i.id);
1643          let multiplier = 0;
1644          if (player_in_squad !== undefined) {
1645            multiplier = player_in_squad.multiplier;
1646          }
1647          let ow = ownership_by_id[i.id];
1648          let eo = ow.effective_ownership / 100;
1649          if (!this.is_using_sample) {
1650            eo = ow.selected_by_percent / 100;
1651          }
1652          return {
1653            id: i.id,
1654            player: i,
1655            multiplier: multiplier,
1656            xp: i.xp,
1657            rp: i.rp,
1658            eo: eo,
1659          };
1660        });
1661
1662        let split_active_players = _.groupBy(active_players_final, (e) => {
1663          return team_ids.includes(e.id);
1664        });
1665        let team_active = split_active_players[true] || [];
1666        let xp_total_active = getSum(
1667          team_active.map((i) => i.xp * i.multiplier)
1668        );
1669        let rp_total_active = getSum(
1670          team_active.map((i) => i.rp * i.multiplier)
1671        );
1672        let xp_gain_active = getSum(
1673          team_active.map((i) => i.xp * (i.multiplier - i.eo))
1674        );
1675        let rp_gain_active = getSum(
1676          team_active.map((i) => i.rp * (i.multiplier - i.eo))
1677        );
1678
1679        let rest_active = split_active_players[false] || [];
1680        let xp_loss_active = getSum(rest_active.map((i) => i.xp * i.eo));
1681        let rp_loss_active = getSum(rest_active.map((i) => i.rp * i.eo));
1682
1683        xp_total += xp_total_active;
1684        xp_gain += xp_gain_active;
1685        xp_loss += xp_loss_active;
1686        xp_diff = xp_gain - xp_loss;
1687        average_expected += getSum(
1688          active_players_final.map((i) => i.xp * i.eo)
1689        );
1690
1691        rp_total += rp_total_active;
1692        rp_gain += rp_gain_active;
1693        rp_loss += rp_loss_active;
1694        rp_diff = rp_gain - rp_loss;
1695        average_realized += getSum(
1696          active_players_final.map((i) => i.rp * i.eo)
1697        );
1698      }
1699
1700      let proj_total = 0;
1701      if (time <= this.now_dt) {
1702        proj_total = rp_total + setoff.team;
1703        proj_avg = average_realized + setoff.avg;
1704        proj_gain = rp_gain + setoff.team;
1705        proj_loss = rp_loss + setoff.avg;
1706        proj_diff = rp_diff + setoff.team - setoff.avg;
1707      } else {
1708        // future date-time
1709        proj_total =
1710          rp_total +
1711          setoff.team +
1712          (xp_total + setoff.team) -
1713          now_values.xp_total;
1714        proj_avg =
1715          average_realized +
1716          setoff.avg +
1717          (average_expected + setoff.avg) -
1718          now_values.avg_expected;
1719        proj_gain =
1720          rp_gain + setoff.team + (xp_gain + setoff.team) - now_values.xp_gain;
1721        proj_loss =
1722          rp_loss + setoff.avg + (xp_loss + setoff.avg) - now_values.xp_loss;
1723        proj_diff =
1724          rp_diff +
1725          setoff.team -
1726          setoff.avg +
1727          (xp_diff + setoff.team - setoff.avg) -
1728          now_values.xp_diff;
1729      }
1730
1731      return {
1732        xp_total: xp_total + setoff.team,
1733        rp_total: rp_total + setoff.team,
1734        xp_gain: xp_gain + setoff.team,
1735        rp_gain: rp_gain + setoff.team,
1736        xp_loss: xp_loss + setoff.avg,
1737        rp_loss: rp_loss + setoff.avg,
1738        xp_diff: xp_diff + setoff.team - setoff.avg,
1739        rp_diff: rp_diff + setoff.team - setoff.avg,
1740        avg_expected: average_expected + setoff.avg,
1741        avg_realized: average_realized + setoff.avg,
1742        proj_total,
1743        proj_avg,
1744        proj_gain,
1745        proj_loss,
1746        proj_diff,
1747      };
1748    },
1749    toggleHit() {
1750      this.is_using_hits = !this.is_using_hits;
1751      this.$nextTick(() => {
1752        refresh_all_graphs();
1753      });
1754    },
1755    toggleShowDetails() {
1756      this.show_details = !this.show_details;
1757      this.$nextTick(() => {
1758        refresh_all_graphs();
1759      });
1760    },
1761    openModalFor(id) {
1762      this.modal_selected_game = id;
1763      this.$nextTick(() => {
1764        $("#matchReportModal").modal("show");
1765      });
1766    },
1767    selectCaptain(e) {
1768      let id = e.currentTarget.dataset.id;
1769      let d = _.cloneDeep(this.team_data);
1770      let current_captain = d.picks.find((i) => i.multiplier > 1);
1771      let this_player = d.picks.find((i) => i.element == id);
1772      if (current_captain !== undefined) {
1773        if (current_captain.element == this_player.element) {
1774          this_player.multiplier = 5 - this_player.multiplier;
1775        } else {
1776          this_player.multiplier = current_captain.multiplier + 0;
1777          this_player.is_captain = true;
1778          current_captain.is_captain = false;
1779          current_captain.multiplier = 1;
1780        }
1781      } else {
1782        this_player.multiplier = 2;
1783        this_player.is_captain = true;
1784      }
1785      this.team_data = Object.freeze(d);
1786    },
1787    toggleBench(e) {
1788      let id = e.currentTarget.dataset.id;
1789      let d = _.cloneDeep(this.team_data);
1790      let this_player = d.picks.find((i) => i.element == id);
1791      if (this_player.multiplier > 0) {
1792        this_player.multiplier = 0;
1793        this_player.is_captain = false;
1794      } else {
1795        this_player.multiplier = 1;
1796      }
1797      this.team_data = Object.freeze(d);
1798    },
1799    tagForTransfer(e) {
1800      let id = e.currentTarget.dataset.id;
1801      $("#all_players_table").DataTable().destroy();
1802      this.$nextTick(() => {
1803        if (this.target_player !== undefined) {
1804          let current_selected = this.target_player.id;
1805          if (current_selected == id) {
1806            this.target_player = undefined;
1807            return;
1808          }
1809        }
1810        this.target_player = this.element_data_combined[parseInt(id)];
1811        this.$nextTick(() => {
1812          $("#all_players_table").DataTable({
1813            order: [[3, "desc"]],
1814            lengthChange: false,
1815            pageLength: window.screen.width <= 768 ? 5 : 15,
1816            columnDefs: [{ orderable: false, targets: 1 }],
1817          });
1818        });
1819      });
1820    },
1821    performSwap(e) {
1822      let id = e.currentTarget.dataset.id;
1823      $("#all_players_table").DataTable().destroy();
1824      this.$nextTick(() => {
1825        this.target_player = undefined;
1826      });
1827      let d = _.cloneDeep(this.team_data);
1828      let current_player = d.picks.find(
1829        (i) => i.element == this.target_player.id
1830      );
1831      if (current_player !== undefined) {
1832        current_player.element = parseInt(id);
1833      }
1834      this.team_data = Object.freeze(d);
1835    },
1836    applyAutosubtoTeam() {
1837      if (!this.is_ready) {
1838        return;
1839      }
1840      let el_data = _.cloneDeep(this.el_data);
1841      let autosubs = [];
1842      let rp_by_id = this.rp_by_id;
1843      el_data.forEach((e) => {
1844        autosubs.push([
1845          e.id,
1846          {
1847            element_type: e.element_type,
1848            autosub:
1849              rp_by_id && rp_by_id[e.id] ? rp_by_id[e.id].autosub : false,
1850          },
1851        ]);
1852      });
1853      // let autosubs = Object.fromEntries(Object.values(rp_by_id).map(i => [i.id, { autosub: i.autosub, element_type: i.element_type }]))
1854      let autosub_dict = Object.fromEntries(autosubs);
1855      this.team_data.picks = autosubbed_team(
1856        this.team_data.picks,
1857        autosub_dict
1858      ).team;
1859    },
1860    toggleAutoSub() {
1861      this.is_using_autosub = !this.is_using_autosub;
1862      if (this.is_using_autosub) {
1863        this.applyAutosubtoTeam();
1864      }
1865      this.$nextTick(() => {
1866        refresh_all_graphs();
1867      });
1868    },
1869    sortData(e) {
1870      let tag = e.currentTarget.dataset.tag;
1871      let source = jQuery(e.currentTarget).closest("table").data("source");
1872      let last_sort = this[source + "_last_sort"];
1873      let new_order;
1874      let new_value;
1875      if (tag == last_sort) {
1876        new_order = _.orderBy(this[source], tag, "desc");
1877        new_value = undefined;
1878      } else {
1879        new_order = _.orderBy(this[source], tag, "asc");
1880        new_value = tag;
1881      }
1882      this[source] = new_order;
1883      this[source + "_last_sort"] = new_value;
1884    },
1885    breakpoint() {
1886      debugger;
1887    },
1888    toggleWhiteSpace() {
1889      this.no_white_space = !this.no_white_space;
1890      refresh_all_graphs();
1891    },
1892    addNewOverride() {
1893      let player = this.dropdown_select;
1894      if (player == "") {
1895        return;
1896      }
1897      let rp = parseInt($("#newRpEnter").val()) || 0;
1898      this.addOverride(player, rp);
1899    },
1900    addOverride(player, rp) {
1901      let b = _.cloneDeep(this.override_rp);
1902      let e = b.find((i) => i.id == player);
1903      if (e) {
1904        e.rp = rp;
1905      } else {
1906        b.push({ id: player, rp: rp });
1907      }
1908      this.override_rp = b;
1909
1910      // this.$nextTick(() => {
1911      //     refresh_all_graphs()
1912      // })
1913    },
1914    deleteOverride(entry_id) {
1915      this.override_rp = this.override_rp.filter((i) => i.id != entry_id);
1916      // this.$nextTick(() => {
1917      //     refresh_all_graphs()
1918      // })
1919    },
1920    clearOverrides() {
1921      this.override_rp = [];
1922      // this.$nextTick(() => {
1923      //     refresh_all_graphs()
1924      // })
1925    },
1926    atCloseOverride() {
1927      this.$nextTick(() => {
1928        refresh_all_graphs();
1929      });
1930    },
1931  },
1932});
1933
1934async function load_team_data(graph_refresh = false) {
1935  if (app.team_id == -1 || app.team_id == -2) {
1936    return;
1937  }
1938
1939  await get_team_picks({
1940    gw: app.gw.slice(2),
1941    team_id: app.team_id,
1942    force_last_gw: true,
1943  })
1944    .then((response) => {
1945      app.saveTeamData(response.body);
1946      if (app.gw == app.next_gw && app.last_gw_data_marker) {
1947        app.findIdealPicks();
1948      }
1949      app.using_last_gw_team = response.is_last_gw;
1950      if (graph_refresh) {
1951        refresh_all_graphs();
1952      }
1953    })
1954    .catch((error) => {
1955      console.error(error);
1956      app.loadOptimal();
1957    });
1958
1959  return get_team_info(app.team_id)
1960    .then((data) => {
1961      app.saveTeamInfo(data);
1962    })
1963    .catch((error) => {
1964      console.error(error);
1965    });
1966}
1967
1968async function load_element_data() {
1969  return get_cached_element_data({
1970    season: app.season,
1971    gw: app.gw,
1972    date: app.date,
1973  })
1974    .then((data) => {
1975      app.saveEl(data);
1976    })
1977    .catch((error) => {
1978      console.error(error);
1979    });
1980}
1981
1982async function load_xp_data() {
1983  return getXPData_Fernet({ season: app.season, gw: app.gw, date: app.date })
1984    .then((data) => {
1985      app.saveXP(data);
1986    })
1987    .catch((error) => {
1988      console.error(error);
1989    });
1990}
1991
1992async function load_rp_data() {
1993  return getRPData(app.gw.slice(2))
1994    .then((data) => {
1995      app.saveRP(data);
1996    })
1997    .catch((error) => {
1998      app.saveRP([]);
1999      console.error(error);
2000    });
2001}
2002
2003async function load_sample_data() {
2004  return get_sample_data(app.season, parseInt(app.gw.slice(2)))
2005    .then((data) => {
2006      if (data[0].status == "rejected") {
2007        app.is_using_autosub = false;
2008        app.is_using_hits = false;
2009        get_sample_data(app.season, parseInt(app.gw.slice(2)) - 1).then(
2010          (data) => {
2011            if (data[0].status == "rejected") {
2012              app.saveSampleData(false, []);
2013            } else {
2014              let sample_data = data[0].value;
2015              //   if (data[1].status != "rejected") {
2016              //     sample_data["Prime"] = data[1].value;
2017              //   }
2018              app.last_gw_data_marker = true;
2019              app.saveSampleData(true, sample_data);
2020            }
2021          }
2022        );
2023        //app.saveSampleData(false, []);
2024      } else {
2025        let sample_data = data[0].value;
2026        // if (data[1].status != "rejected") {
2027        //   sample_data["Prime"] = data[1].value;
2028        // }
2029        app.last_gw_data_marker = false;
2030        app.saveSampleData(true, sample_data);
2031      }
2032    })
2033    .catch((error) => {
2034      // Delete sample data and force official FPL API values
2035      app.saveSampleData(false, []);
2036    });
2037}
2038
2039async function load_fixture_data() {
2040  return get_fixture(app.gw.slice(2))
2041    .then((data) => {
2042      app.saveFixtureData(data);
2043    })
2044    .catch((error) => {
2045      console.error(error);
2046    });
2047}
2048
2049let axis_functions = {};
2050let target_stat = {};
2051
2052// Theme-aware color function
2053function getThemeColors() {
2054  const isLightMode =
2055    document.documentElement.getAttribute("data-theme") === "light" ||
2056    document.body.getAttribute("data-theme") === "light";
2057
2058  if (isLightMode) {
2059    return {
2060      background: "#f5f5f5",
2061      graphBackground: "#ededed",
2062      text: "#2d2d30",
2063      gridLines: "#e0e0e0",
2064      axisText: "#2d2d30",
2065
2066      // Line colors for light mode (darker, more saturated)
2067      lineExpected: "#4CAF50", // Green
2068      lineRealized: "#2196F3", // Blue
2069      lineProjected: "#6366F1", // Indigo
2070      lineAverage: "#EC4899", // Pink
2071      lineYourTeam: "#F59E0B", // Amber
2072      lineFieldAvg: "#EF4444", // Red
2073
2074      // Additional colors
2075      nowLine: "#10B981", // Emerald
2076      zeroLine: "#DC2626", // Red
2077      markerLine: "#8B5CF6", // Purple
2078
2079      // Opacity values
2080      lineOpacity: 1,
2081      gridOpacity: 0.3,
2082      textOpacity: 0.8,
2083    };
2084  } else {
2085    return {
2086      background: "#5a5d5c",
2087      graphBackground: "#5a5d5c",
2088      text: "white",
2089      gridLines: "#f1f1f1",
2090      axisText: "white",
2091
2092      // Line colors for dark mode (original colors)
2093      lineExpected: "#dbf7b4",
2094      lineRealized: "#6fcfd6",
2095      lineProjected: "#587ee0b5",
2096      lineAverage: "#e482baa8",
2097      lineYourTeam: "#ffc356",
2098      lineFieldAvg: "#de6363",
2099
2100      // Additional colors
2101      nowLine: "#60ffc9",
2102      zeroLine: "#4c0000",
2103      markerLine: "#f9aeff91",
2104
2105      // Opacity values
2106      lineOpacity: 0.8,
2107      gridOpacity: 0.5,
2108      textOpacity: 0.65,
2109    };
2110  }
2111}
2112
2113function init_timeline() {
2114  if (!app.is_fixture_ready) {
2115    return;
2116  }
2117
2118  let graph_id = "timeline";
2119  const colors = getThemeColors();
2120
2121  var margin = { top: 9, right: 5, bottom: 20, left: 5 },
2122    width = 450 - margin.left - margin.right,
2123    // height = 20 + 20 * (app.gameweek_info.channels + 1) - margin.top - margin.bottom;
2124    height = 2 + 13 * (app.gameweek_info.channels + 1);
2125
2126  let cnv = d3
2127    .select("#d3-timeline")
2128    .append("svg")
2129    .attr("id", graph_id)
2130    .attr(
2131      "viewBox",
2132      `0 0  ${width + margin.left + margin.right} ${
2133        height + margin.top + margin.bottom
2134      }`
2135    )
2136    .attr("class", "pull-center active-graph")
2137    .style("display", "block")
2138    .style("min-width", "300px")
2139    .style("padding-bottom", "10px");
2140
2141  let svg = cnv
2142    .append("g")
2143    .attr("class", "svg-actual")
2144    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
2145  let grayrect = svg.append("g");
2146  grayrect
2147    .append("rect")
2148    .attr("fill", colors.graphBackground)
2149    .attr("width", width)
2150    .attr("height", height);
2151
2152  let game_color = (d) => {
2153    return d3.schemeDark2[d % 8];
2154  };
2155
2156  // Min max values
2157  let vals = app.gameweek_games_with_metadata;
2158  let x_high = app.gameweek_info.end_dt.getTime() + 120 * 60 * 1000;
2159  let x_low = app.gameweek_info.start_dt.getTime() - 120 * 60 * 1000;
2160
2161  let x_ticks = [];
2162  let start_midnight = new Date(app.gameweek_info.start_dt.getTime());
2163  start_midnight.setHours(24, 0, 0, 0);
2164  for (
2165    let i = start_midnight.getTime();
2166    i <= x_high;
2167    i += 24 * 60 * 60 * 1000
2168  ) {
2169    x_ticks.push(i);
2170  }
2171
2172  // Axis-x
2173  var x = d3.scaleLinear().domain([x_low, x_high]).range([0, width]);
2174  svg
2175    .append("g")
2176    // .attr('transform', 'translate(0,' + height + ')')
2177    .call(
2178      d3
2179        .axisBottom(x)
2180        .tickValues(x_ticks)
2181        .tickFormat(function (d, i) {
2182          return new Date(d).toLocaleDateString();
2183        })
2184        .tickSize(height)
2185    );
2186
2187  axis_functions[graph_id] = {};
2188  axis_functions[graph_id].x = x;
2189
2190  // Axis -y
2191  var y = d3
2192    .scaleBand()
2193    .domain(vals.map((i) => i.order))
2194    .range([height, 0])
2195    .paddingInner(0.1)
2196    .paddingOuter(0.05);
2197  svg.append("g").call(d3.axisLeft(y).tickSize(0).tickValues([]));
2198
2199  axis_functions[graph_id].y = y;
2200
2201  svg
2202    .call((g) => g.selectAll(".tick text").attr("fill", colors.axisText))
2203    .call((g) =>
2204      g
2205        .selectAll(".tick line")
2206        .attr("stroke-dasharray", "4,2")
2207        .attr("stroke", colors.gridLines)
2208        .attr("stroke-width", 0.5)
2209        .attr("opacity", 0.3)
2210        .style("pointer-events", "none")
2211    )
2212    .call((g) => g.selectAll(".domain").attr("opacity", 0));
2213
2214  // Title - x
2215  svg
2216    .append("text")
2217    .attr("text-anchor", "middle")
2218    .attr("x", width / 2)
2219    .attr("y", height + 17)
2220    .attr("font-size", "4pt")
2221    .attr("fill", colors.text)
2222    .text("Date/Time");
2223
2224  svg.call((s) => s.selectAll(".tick").attr("font-size", "4pt"));
2225
2226  var Tooltip = d3
2227    .select("#timeline-wrapper")
2228    .append("div")
2229    .style("opacity", 0)
2230    .attr("class", "tooltip bg-dark text-white")
2231    .attr("id", "timeline-tooltip")
2232    .style("background-color", "white")
2233    .style("border", "solid")
2234    .style("border-width", "2px")
2235    .style("border-radius", "5px")
2236    .style("padding", "5px")
2237    .style("color", "black");
2238
2239  function mouseclick(d) {
2240    let game = d3.select(this);
2241    app.modal_selected_game = game.attr("data-index");
2242    app.$nextTick(() => {
2243      Tooltip.style("opacity", 0).style("left", "0px").style("top", "0px");
2244      $("#matchReportModal").modal("show");
2245    });
2246  }
2247
2248  function mouseenter(d) {
2249    let game = d3.select(this);
2250    game.attr("opacity", 1);
2251    game.attr("fill", "red");
2252    // app.selected_game = game.attr('data-index');
2253    Tooltip.style("opacity", 0.85);
2254  }
2255
2256  function mouseleave(d) {
2257    let game = d3.select(this);
2258    game.attr("opacity", 0.7);
2259    game.attr("fill", game_color(game.attr("data-index")));
2260    // app.selected_game = undefined;
2261    Tooltip.style("opacity", 0).style("left", "0px").style("top", "0px");
2262  }
2263
2264  function mousemove(e, d) {
2265    let offset = -20;
2266    try {
2267      offset = -Tooltip.node().getBoundingClientRect().width / 2;
2268    } catch {}
2269    team_lines = "";
2270
2271    if (app.is_ready) {
2272      if (d.started) {
2273        team_lines = `
2274                <tr><td class="text-right">xP Net</td><td>${getWithSign(
2275                  d.xp_team_net
2276                )}</td></tr>
2277                <tr><td class="text-right">rP Net</td><td>${getWithSign(
2278                  d.rp_team_net
2279                )}</td></tr>
2280                `;
2281      } else {
2282        team_lines = `
2283                <tr><td class="text-right">xP Gain</td><td>${getWithSign(
2284                  d.xp_team_gain
2285                )}</td></tr>
2286                <tr><td class="text-right">xP Loss</td><td>${getWithSign(
2287                  d.xp_team_loss
2288                )}</td></tr>
2289                <tr><td class="text-right">xP Net</td><td>${getWithSign(
2290                  d.xp_team_net
2291                )}</td></tr>
2292                `;
2293      }
2294    }
2295
2296    if (d.finished_provisional) {
2297      Tooltip.html(
2298        `<div class="tooltip-inside text-small"><b>${d.game_string}</b><br/>
2299                <table class="table table-dark table-sm table-striped mb-0">
2300                    <tr><td class="text-right">Total xP</td><td>${d.xp_sum.toFixed(
2301                      2
2302                    )}</td></tr>
2303                    <tr><td class="text-right">Total rP</td><td>${
2304                      d.rp_sum
2305                    }</td></tr>
2306                    ${team_lines}
2307                </table></div>`
2308      );
2309    } else {
2310      Tooltip.html(
2311        `<div class="tooltip-inside text-small"><b>${d.game_string}</b><br/>
2312                <table class="table table-dark table-sm table-striped mb-0">
2313                    <tr><td class="text-right">Total xP</td><td>${d.xp_sum.toFixed(
2314                      2
2315                    )}</td></tr>
2316                    ${team_lines}
2317                </table></div>`
2318      );
2319    }
2320
2321    Tooltip.style("left", e.pageX + offset + "px").style(
2322      "top",
2323      e.pageY + 30 + "px"
2324    );
2325  }
2326
2327  // Games
2328  grayrect
2329    .append("g")
2330    .selectAll()
2331    .data(vals)
2332    .enter()
2333    .append("rect")
2334    // .attr("transform", function(d) { return "translate(" + x(d.node_info.start) + ",0)"; })
2335    .attr("x", (d) => x(d.node_info.start))
2336    .attr("width", (d) => x(d.node_info.end) - x(d.node_info.start))
2337    .attr("y", (d) => y(d.order))
2338    .attr("height", y.bandwidth())
2339    .attr("opacity", 0.7)
2340    .attr("fill", (d, i) => game_color(i))
2341    .attr("stroke", colors.gridLines)
2342    .attr("stroke-width", 0.5)
2343    .attr("data-index", (d, i) => i)
2344    .style("pointer-events", "all")
2345    .style("cursor", "pointer")
2346    .on("mouseenter", mouseenter)
2347    .on("mouseleave", mouseleave)
2348    .on("mousemove", mousemove)
2349    .on("click", mouseclick);
2350
2351  svg
2352    .append("g")
2353    .selectAll()
2354    .data(vals)
2355    .enter()
2356    .append("text")
2357    .attr("text-anchor", "end")
2358    .attr("alignment-baseline", "hanging")
2359    .attr("x", (d) => x(d.node_info.end) - 1) // (x(d.node_info.end) + x(d.node_info.start)) / 2)
2360    .attr("y", (d) => y(d.order) + 1) //y(d.order) + y.bandwidth() / 3)
2361    .attr(
2362      "font-size",
2363      Math.max(Math.min(Math.round((7 * 1e9) / (x_high - x_low)) / 16, 7), 2) +
2364        "pt"
2365    )
2366    .attr("fill", colors.text)
2367    .style("pointer-events", "none")
2368    .text((d, i) => i + 1); //"ID: " + (i + 1));
2369
2370  if (app.is_ready) {
2371    svg
2372      .append("g")
2373      .selectAll()
2374      .data(vals)
2375      .enter()
2376      .append("text")
2377      .attr("text-anchor", "middle")
2378      .attr("alignment-baseline", "middle")
2379      .attr("x", (d) => (x(d.node_info.end) + x(d.node_info.start)) / 2)
2380      .attr("y", (d) => y(d.order) + y.bandwidth() * 0.65)
2381      .attr(
2382        "font-size",
2383        Math.max(
2384          Math.min(Math.round((14 * 1e9) / (x_high - x_low)) / 13, 8),
2385          4
2386        ) + "pt"
2387      )
2388      .attr("fill", colors.text)
2389      .style("pointer-events", "none")
2390      .text((d) => d.players_owned);
2391  }
2392
2393  let right_now = new Date(app.now_dt);
2394  let now_time = right_now.getTime();
2395
2396  let now_group = svg.append("g");
2397  now_group
2398    .append("line")
2399    .attr("id", "now")
2400    .attr("pointer-events", "none")
2401    .attr("x1", x(now_time))
2402    .attr("y1", y.range()[0])
2403    .attr("x2", x(now_time))
2404    .attr("y2", y.range()[1] - 2)
2405    .style("stroke", colors.nowLine)
2406    .style("stroke-dasharray", "2,0.5")
2407    .style("stroke-width", 1)
2408    .style("stroke-opacity", 0.7);
2409
2410  now_group
2411    .append("text")
2412    .attr("text-anchor", "middle")
2413    .attr("x", x(now_time))
2414    .attr("y", -6)
2415    .attr("font-size", "3pt")
2416    .attr("fill", colors.text)
2417    .text("Now");
2418
2419  grayrect.on("mouseenter.foo", (e) => {
2420    synced_enter(e, graph_id);
2421  });
2422  grayrect.on("mousemove.foo", (e) => {
2423    synced_move(e, graph_id);
2424  });
2425  grayrect.on("mouseleave.foo", (e) => {
2426    synced_leave();
2427  });
2428  grayrect.on("dblclick", (e) => {
2429    synced_mark(e, graph_id);
2430  });
2431
2432  try {
2433    let left_offset =
2434      document.querySelector("#now").getBoundingClientRect().x -
2435      window.innerWidth / 2;
2436    $("#d3-timeline").scrollLeft(left_offset);
2437  } catch {}
2438}
2439
2440async function draw_user_graph(options = {}) {
2441  return new Promise((resolve, reject) => {
2442    // graph-wrapper-points
2443
2444    if (!app.is_ready) {
2445      resolve("Not ready");
2446    }
2447
2448    let graph_id = "graph-" + options.stat;
2449    target_stat[graph_id] = options.stat;
2450    const colors = getThemeColors();
2451
2452    var margin = { top: 25, right: 5, bottom: 20, left: 15 },
2453      width = 250 - margin.left - margin.right,
2454      height = 180 - margin.top - margin.bottom;
2455
2456    let cnv = d3
2457      .select(options.target)
2458      .append("svg")
2459      .attr("id", graph_id)
2460      .attr(
2461        "viewBox",
2462        `0 0  ${width + margin.left + margin.right} ${
2463          height + margin.top + margin.bottom
2464        }`
2465      )
2466      .attr("class", "pull-center active-graph")
2467      .style("display", "block")
2468      .style("padding-bottom", "10px");
2469
2470    let svg = cnv
2471      .append("g")
2472      .attr("class", "svg-actual")
2473      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
2474    let grayrect = svg.append("g");
2475    grayrect
2476      .append("rect")
2477      .attr("fill", colors.graphBackground)
2478      .attr("width", width)
2479      .attr("height", height);
2480
2481    // Min max values
2482    let data = _.cloneDeep(app.get_graph_checkpoints);
2483    data = data.checkpoints;
2484    if (data.length == 0) {
2485      resolve("No data");
2486    }
2487
2488    let x_high = data[data.length - 1].time;
2489    let x_low = data[0].time;
2490
2491    let y_low = Math.min(
2492      ...data
2493        .map((i) => i.expected[options.stat])
2494        .concat(data.map((i) => i.realized[options.stat]))
2495        .concat(data.map((i) => i.projected[options.stat]))
2496    );
2497    y_low = Math.min(y_low, 0);
2498    let y_high = Math.max(
2499      ...data
2500        .map((i) => i.expected[options.stat])
2501        .concat(data.map((i) => i.realized[options.stat]))
2502        .concat(data.map((i) => i.projected[options.stat]))
2503    );
2504    // y_high = Math.max(y_high * 1.1, y_high + 5)
2505
2506    if (options.stat == "points") {
2507      let y_avg_high = Math.max(
2508        ...data
2509          .map((i) => i.average.expected)
2510          .concat(data.map((i) => i.average.realized))
2511          .concat(data.map((i) => i.projected.avg))
2512      );
2513      // y_avg_high = Math.max(y_avg_high * 1.1, y_avg_high + 5)
2514      let y_avg_low = Math.min(
2515        ...data
2516          .map((i) => i.average.expected)
2517          .concat(data.map((i) => i.average.realized))
2518          .concat(data.map((i) => i.projected.avg))
2519      );
2520      y_high = Math.max(y_high, y_avg_high);
2521      y_low = Math.min(y_low, y_avg_low);
2522    }
2523
2524    let x_ticks = [];
2525    let start_midnight = new Date(app.gameweek_info.start_dt.getTime());
2526    start_midnight.setHours(24, 0, 0, 0);
2527    for (
2528      let i = start_midnight.getTime();
2529      i <= x_high;
2530      i += 24 * 60 * 60 * 1000
2531    ) {
2532      x_ticks.push(i);
2533    }
2534
2535    // Axis-x
2536    var x = d3.scaleLinear().domain([x_low, x_high]).range([0, width]);
2537
2538    // no_white_space
2539    if (app.no_white_space == true) {
2540      let intervals = _.cloneDeep(app.get_graph_checkpoints.intervals);
2541      let sum_intervals = getSum(intervals.map((i) => i[1] - i[0]));
2542      let c = 0;
2543      intervals.forEach((i) => {
2544        i.start = c;
2545        i.finish = c + i[1] - i[0];
2546        i.start_ratio = i.start / sum_intervals;
2547        i.finish_ratio = i.finish / sum_intervals;
2548        c = i.finish;
2549      });
2550      let ratios = intervals.map((i) => [i.start_ratio, i.finish_ratio]).flat();
2551      let domain = intervals.map((i) => [i[0], i[1]]).flat();
2552      let range = ratios.map((i) => width * i);
2553      // let first = intervals[0][0]
2554      x = d3.scaleLinear().domain(domain).range(range);
2555      x_low = intervals[0][0];
2556      x_high = intervals[intervals.length - 1][1];
2557      data = data.filter((i) => i.reason != "init" && i.reason != "final");
2558    }
2559    // var x = d3.scaleLinear().domain([x_low, x_high]).range([0, width]);
2560    svg
2561      .append("g")
2562      // .attr('transform', 'translate(0,' + height + ')')
2563      .call(
2564        d3
2565          .axisBottom(x)
2566          .tickValues(x_ticks)
2567          .tickFormat(function (d, i) {
2568            return new Date(d).toLocaleDateString();
2569          })
2570          .tickSize(height)
2571      );
2572
2573    axis_functions[graph_id] = {};
2574    axis_functions[graph_id].x = x;
2575
2576    // Axis -y
2577    // var y = d3.scaleBand().domain(vals.map(i => i.order)).range([height, 0]).paddingInner(0.1).paddingOuter(0.05);
2578    // svg.append('g').call(d3.axisLeft(y).tickSize(0).tickValues([]));
2579
2580    let top_gap = 22; // 18
2581    y_high = ((y_high - y_low) / (height - top_gap)) * height + y_low;
2582
2583    // svg.append('rect')
2584    //     .attr("x", 0 )
2585    //     .attr("y", 0)
2586    //     .attr('fill', 'red')
2587    //     .attr('width', width)
2588    //     .attr('height', top_gap);
2589
2590    var y = d3.scaleLinear().domain([y_low, y_high]).range([height, 0]);
2591    svg
2592      .append("g")
2593      .attr("transform", "translate(" + width + ",0)")
2594      .call(d3.axisLeft(y).tickSize(width));
2595
2596    axis_functions[graph_id].y = y;
2597
2598    svg
2599      .call((g) => g.selectAll(".tick text").attr("fill", colors.axisText))
2600      .call((g) =>
2601        g
2602          .selectAll(".tick line")
2603          .attr("stroke-dasharray", "4,2")
2604          .attr("stroke", colors.gridLines)
2605          .attr("stroke-width", 0.5)
2606          .attr("opacity", 0.1)
2607          .style("pointer-events", "none")
2608      )
2609      .call((g) => g.selectAll(".domain").attr("opacity", 0));
2610
2611    // Title - x
2612    svg
2613      .append("text")
2614      .attr("text-anchor", "middle")
2615      .attr("x", width / 2)
2616      .attr("y", height + 17)
2617      .attr("font-size", "4pt")
2618      .attr("fill", colors.text)
2619      .text("Date/Time");
2620
2621    svg.call((s) => s.selectAll(".tick").attr("font-size", "4pt"));
2622
2623    svg
2624      .append("g")
2625      .append("line")
2626      .attr("x1", x(x_low))
2627      .attr("y1", y(0))
2628      .attr("x2", x(x_high))
2629      .attr("y2", y(0))
2630      .style("stroke", colors.zeroLine)
2631      .style("stroke-opacity", 0.4)
2632      .style("stroke-width", 1)
2633      .style("pointer-events", "none");
2634
2635    let right_now = new Date(app.now_dt);
2636    let now_time = right_now.getTime();
2637
2638    let game_data = app.gameweek_games_with_metadata.map((i) => i.node_info);
2639    svg
2640      .selectAll()
2641      .data(game_data)
2642      .enter()
2643      .append("rect")
2644      .attr("x", (d) => x(d.start))
2645      .attr("y", (d) => y(y_high))
2646      .attr("width", (d) => x(d.end) - x(d.start))
2647      .attr("height", (d) =>
2647 height - y(y_high))
2648      .attr("class", "game_bar");
2649
2650    // Now line
2651    let now_group = svg.append("g");
2652    now_group
2653      .append("line")
2654      .attr("id", "now")
2655      .attr("pointer-events", "none")
2656      .attr("x1", x(now_time))
2657      .attr("y1", y(y_low))
2658      .attr("x2", x(now_time))
2659      .attr("y2", y(y_high))
2660      .style("stroke", colors.nowLine)
2661      .style("stroke-dasharray", "2,0.5")
2662      .style("opacity", 0.3)
2663      .style("stroke-width", 1)
2664      .style("stroke-opacity", 0.7);
2665
2666    now_group
2667      .append("text")
2668      .attr("text-anchor", "middle")
2669      .attr("x", x(now_time))
2670      .attr("y", -6)
2671      .attr("font-size", "3pt")
2672      .attr("fill", "white")
2673      .text("Now");
2674
2675    svg
2676      .append("text")
2677      .attr("text-anchor", "middle")
2678      .attr("x", width / 2)
2679      .attr("y", -15)
2680      .attr("font-size", "5pt")
2681      .attr("fill", colors.text)
2682      .text(options.title);
2683
2684    grayrect.on("mouseenter.foo", (e) => {
2685      synced_enter(e, graph_id);
2686    });
2687    grayrect.on("mousemove.foo", (e) => {
2688      synced_move(e, graph_id);
2689    });
2690    grayrect.on("mouseleave.foo", (e) => {
2691      synced_leave();
2692    });
2693    grayrect.on("dblclick", (e) => {
2694      synced_mark(e, graph_id);
2695    });
2696
2697    // expected values line
2698
2699    svg
2700      .append("path")
2701      .datum(data)
2702      .attr("fill", "none")
2703      .attr("stroke", colors.lineExpected)
2704      .attr("stroke-opacity", colors.lineOpacity)
2705      .style("stroke-dasharray", "2,0.5")
2706      .attr("stroke-width", 1)
2707      .style("pointer-events", "none")
2708      .attr(
2709        "d",
2710        d3
2711          .line()
2712          .x((d) => x(d.time))
2713          .y((d) => y(d.expected[options.stat]))
2714      );
2715
2716    let realized_data = _.cloneDeep(data);
2717    const now_index = realized_data.findIndex((i) => i.reason == "now");
2718    realized_data = realized_data.slice(0, now_index + 1);
2719
2720    // realized values line
2721
2722    svg
2723      .append("path")
2724      .datum(realized_data)
2725      .attr("fill", "none")
2726      .attr("stroke", colors.lineRealized)
2727      .attr("stroke-opacity", colors.lineOpacity)
2728      .attr("stroke-width", 1)
2729      .style("pointer-events", "none")
2730      .attr(
2731        "d",
2732        d3
2733          .line()
2734          .x((d) => x(d.time))
2735          .y((d) => y(d.realized[options.stat]))
2736      );
2737
2738    // projected
2739
2740    let future_data = _.cloneDeep(data);
2741    future_data = future_data.slice(now_index);
2742
2743    svg
2744      .append("path")
2745      .datum(future_data)
2746      .attr("fill", "none")
2747      .attr("stroke", colors.lineProjected)
2748      .attr("stroke-opacity", colors.lineOpacity)
2749      .attr("stroke-dasharray", "5,1")
2750      .attr("stroke-width", 1)
2751      .style("pointer-events", "none")
2752      .attr(
2753        "d",
2754        d3
2755          .line()
2756          .x((d) => x(d.time))
2757          .y((d) => y(d.projected[options.stat]))
2758      );
2759
2760    if (options.stat == "points") {
2761      svg
2762        .append("path")
2763        .datum(future_data)
2764        .attr("fill", "none")
2765        .attr("stroke", colors.lineAverage)
2766        .attr("stroke-opacity", colors.lineOpacity)
2767        .attr("stroke-dasharray", "5,1")
2768        .attr("stroke-width", 1)
2769        .style("pointer-events", "none")
2770        .attr(
2771          "d",
2772          d3
2773            .line()
2774            .x((d) => x(d.time))
2775            .y((d) => y(d.projected.avg))
2776        );
2777
2778      svg
2779        .append("path")
2780        .datum(data)
2781        .attr("fill", "none")
2782        .attr("stroke", colors.lineYourTeam)
2783        .attr("stroke-opacity", colors.lineOpacity)
2784        .style("stroke-dasharray", "2,0.5")
2785        .attr("stroke-width", 1)
2786        .style("pointer-events", "none")
2787        .attr(
2788          "d",
2789          d3
2790            .line()
2791            .x((d) => x(d.time))
2792            .y((d) => y(d.average.expected))
2793        );
2794
2795      svg
2796        .append("path")
2797        .datum(realized_data)
2798        .attr("fill", "none")
2799        .attr("stroke", colors.lineFieldAvg)
2800        .attr("stroke-opacity", colors.lineOpacity)
2801        .attr("stroke-width", 1)
2802        .style("pointer-events", "none")
2803        .attr(
2804          "d",
2805          d3
2806            .line()
2807            .x((d) => x(d.time))
2808            .y((d) => y(d.average.realized))
2809        );
2810    }
2811    reset_graph_values();
2812
2813    if (app.show_details) {
2814      let text_info = svg.append("g");
2815      text_info
2816        .append("text")
2817        .attr("text-anchor", "start")
2818        .attr("x", x(x_low) + 1)
2819        .attr("y", y(y_high) + 5)
2820        .attr("font-size", "3pt")
2821        .attr("fill", colors.text)
2822        .attr("opacity", colors.textOpacity)
2823        .style("pointer-events", "none")
2824        .text("Data: " + app.ownership_source);
2825      text_info
2826        .append("text")
2827        .attr("text-anchor", "start")
2828        .attr("x", x(x_low) + 1)
2829        .attr("y", y(y_high) + 10)
2830        .attr("font-size", "3pt")
2831        .attr("fill", colors.text)
2832        .attr("opacity", colors.textOpacity)
2833        .style("pointer-events", "none")
2834        .text("Hits: " + (app.is_using_hits ? "On" : "Off"));
2835      text_info
2836        .append("text")
2837        .attr("text-anchor", "start")
2838        .attr("x", x(x_low) + 1)
2839        .attr("y", y(y_high) + 15)
2840        .attr("font-size", "3pt")
2841        .attr("fill", colors.text)
2842        .attr("opacity", colors.textOpacity)
2843        .style("pointer-events", "none")
2844        .text("Autosub: " + (app.is_using_autosub ? "On" : "Off"));
2845      //   text_info
2846      //     .append("text")
2847      //     .attr("text-anchor", "start")
2848      //     .attr("x", x(x_low) + 1)
2849      //     .attr("y", y(y_high) + 20)
2850      //     .attr("font-size", "3pt")
2851      //     .attr("fill", app.xp_source == "Solio" ? colors.text : "#81ff00")
2852      //     .attr("opacity", app.xp_source == "Solio" ? colors.textOpacity : 0.8)
2853      //     .style("pointer-events", "none")
2854      //     .text("xP Data: " + app.xp_source);
2855
2856      let pc = app.player_counts;
2857      let played_text = pc.your_played + "/" + pc.your_total;
2858      let avg_text = rounded(pc.avg_played) + "/" + rounded(pc.avg_total);
2859      text_info
2860        .append("text")
2861        .attr("text-anchor", "end")
2862        .attr("x", x(x_high) - 3)
2863        .attr("y", y(y_high) + 5)
2864        .attr("font-size", "3pt")
2865        .attr("fill", "burlywood")
2866        .style("pointer-events", "none")
2867        .style("letter-spacing", "0.15px")
2868        .text("fploptimized.com");
2869      text_info
2870        .append("text")
2871        .attr("text-anchor", "end")
2872        .attr("x", x(x_high) - 3)
2873        .attr("y", y(y_high) + 10)
2874        .attr("font-size", "3pt")
2875        .attr("fill", colors.text)
2876        .attr("opacity", colors.textOpacity)
2877        .style("pointer-events", "none")
2878        .text("Played: " + played_text);
2879      text_info
2880        .append("text")
2881        .attr("text-anchor", "end")
2882        .attr("x", x(x_high) - 3)
2883        .attr("y", y(y_high) + 15)
2884        .attr("font-size", "3pt")
2885        .attr("fill", colors.text)
2886        .attr("opacity", colors.textOpacity)
2887        .style("pointer-events", "none")
2888        .text("Avg Played: " + avg_text);
2889    }
2890
2891    resolve("Done");
2892  });
2893}
2894
2895function draw_tier_ownership_graph() {
2896  // xxx
2897
2898  return new Promise((resolve, reject) => {
2899    if (_.isEmpty(app.sorted_ownership_cache)) {
2900      resolve("Not ready");
2901    }
2902
2903    const colors = getThemeColors();
2904    var margin = { top: 20, bottom: 20, left: 10, right: 10 },
2905      width = 250 - margin.left - margin.right,
2906      height = 150;
2907
2908    jQuery("#ownership-graph").empty();
2909
2910    let cnv = d3
2911      .select("#ownership-graph")
2912      .append("svg")
2913      .attr("id", "ownership-svg")
2914      .attr(
2915        "viewBox",
2916        `0 0  ${width + margin.left + margin.right} ${
2917          height + margin.top + margin.bottom
2918        }`
2919      )
2920      // .attr('class', 'pull-center active-graph')
2921      .style("display", "block")
2922      .style("min-width", "300px");
2923
2924    let svg = cnv
2925      .append("g")
2926      .attr("class", "svg-actual")
2927      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
2928    let grayrect = svg.append("g");
2929    grayrect
2930      .append("rect")
2931      .attr("fill", colors.graphBackground)
2932      .attr("width", width)
2933      .attr("height", height)
2934      .attr("stroke", colors.gridLines)
2935      .attr("stroke-width", "0.5");
2936
2937    // Min max values
2938    let vals = app.sorted_ownership_cache.filter(
2939      (i) => i.xp > 0 && i.ownership >= 0.5
2940    );
2941    let x_high = Math.max(...vals.map((i) => i.ownership)) * 1.2;
2942    let x_low = 0.5;
2943
2944    // Axis-x
2945    // Add X axis
2946    var x = d3.scaleLog().base(2).domain([x_low, x_high]).range([0, width]);
2947    svg
2948      .append("g")
2949      .attr("opacity", 1)
2950      .call(d3.axisBottom(x).tickSize(height))
2951      .call((g) =>
2952        g
2953          .selectAll(".tick line")
2954          .attr("stroke-opacity", 0.1)
2955          .attr("stroke-width", 0.5)
2956          .attr("stroke-dasharray", "3,1")
2957          .style("pointer-events", "none")
2958      );
2959    // .call(g => g.selectAll(".tick text").attr("dy", 11)
2960    // );
2961
2962    // Add X axis label:
2963    svg
2964      .append("text")
2965      .attr("text-anchor", "middle")
2966      .attr("x", width / 2)
2967      .attr("y", height + 15)
2968      .attr("font-size", "4pt")
2969      .attr("fill", colors.text)
2970      .text("Effective Ownership % (Log)");
2971
2972    // Axis -y
2973    let y_high = Math.max(...vals.map((i) => i.xp)) + 0.5;
2974    var y = d3.scaleLinear().domain([0, y_high]).range([height, 0]);
2975    svg
2976      .append("g")
2977      .call(d3.axisRight(y).tickSize(width))
2978      .call((g) => g.selectAll(".tick text").attr("x", -8))
2979      .call((g) =>
2980        g.selectAll(".tick:first-of-type line").style("display", "none")
2981      );
2982
2983    svg
2984      .call((g) => g.selectAll(".tick text").attr("fill", colors.axisText))
2985      .call((g) =>
2986        g
2987          .selectAll(".tick line")
2988          .attr("stroke-dasharray", "3,1")
2989          .attr("stroke-width", 0.5)
2990          .attr("stroke-opacity", 0.1)
2991          .style("pointer-events", "none")
2992      )
2993      .call((g) => g.selectAll(".domain").attr("opacity", 0));
2994
2995    // Add y axis label:
2996    svg
2997      .append("text")
2998      .attr("text-anchor", "left")
2999      .attr("x", -margin.left)
3000      .attr("y", -5)
3001      .attr("font-size", "4pt")
3002      .attr("fill", colors.text)
3003      .text("Expected Points");
3004
3005    // Title
3006    svg
3007      .append("text")
3008      .attr("text-anchor", "middle")
3009      .attr("alignment-baseline", "center")
3010      .attr("dominant-baseline", "center")
3011      .attr("x", width / 2)
3012      .attr("y", -15)
3013      .attr("font-size", "4.5pt")
3014      .attr("fill", colors.text)
3015      .text("Expected Points versus Effective Ownership (Log)");
3016
3017    svg.call((s) => s.selectAll(".tick").attr("font-size", "4pt"));
3018
3019    let players = svg.selectAll().data(vals).enter();
3020
3021    let mouseover = (e) => {
3022      e.stopPropagation();
3023      let pid = e.currentTarget.dataset["id"];
3024      let entry = app.element_data_combined[pid];
3025      app.selected_player = entry;
3026      $(".hovertext").remove();
3027      let t = d3.select(e.target);
3028      svg
3029        .append("text")
3030        .attr("class", "hovertext")
3031        .attr("text-anchor", "middle")
3032        .attr("alignment-baseline", "center")
3033        .attr("dominant-baseline", "center")
3034        .attr("x", t.attr("cx"))
3035        .attr("y", parseFloat(t.attr("cy")) - 5)
3036        .attr("font-size", "3pt")
3037        .style("pointer-events", "none")
3038        .attr("fill", colors.text)
3039        .text(entry.name);
3040    };
3041
3042    let mouseclick = (e) => {
3043      e.stopPropagation();
3044      let pid = e.currentTarget.dataset["id"];
3045      let entry = app.element_data_combined[pid];
3046      let t = d3.select(e.target);
3047      if ($("#p-" + entry.id).length == 0) {
3048        svg
3049          .append("text")
3050          .attr("id", "p-" + entry.id)
3051          .attr("text-anchor", "middle")
3052          .attr("alignment-baseline", "center")
3053          .attr("dominant-baseline", "center")
3054          .attr("x", t.attr("cx"))
3055          .attr("y", parseFloat(t.attr("cy")) - 5)
3056          .attr("font-size", "3pt")
3057          .style("pointer-events", "none")
3058          .attr("fill", "white")
3059          .text(entry.name);
3060      } else {
3061        $("#p-" + entry.id).remove();
3062      }
3063    };
3064
3065    let mouseleave = (e) => {
3066      e.stopPropagation();
3067      app.selected_player = undefined;
3068      $(".hovertext").remove();
3069    };
3070
3071    let if_mobile = window.screen.width < 700;
3072
3073    players
3074      .append("circle")
3075      .attr("class", (d) =>
3076        d.multiplier > 0
3077          ? "svg-circles owned-circle"
3078          : "svg-circles nonowned-circle"
3079      )
3080      .attr("cx", (d) => x(d.ownership))
3081      .attr("cy", (d) =>
3081 y(d.xp))
3082      .attr("r", if_mobile ? 3.5 : 2)
3083      .attr("data-id", (d) => d.id)
3084      .on("mouseover", mouseover)
3085      .on("click", mouseclick)
3086      .on("mouseleave", mouseleave);
3087
3088    resolve();
3089  });
3090}
3091
3092function reset_graph_values() {
3093  let graph_types = ["graph-points", "graph-diff", "graph-gain", "graph-loss"];
3094  let x_raw = app.now_dt;
3095  if (app.marked_dt !== undefined) {
3096    x_raw = app.marked_dt;
3097  }
3098  for (let i of graph_types) {
3099    update_graph_hover_values(x_raw, i, true);
3100  }
3101}
3102
3103function update_graph_hover_values(x_raw, gid, reset = false) {
3104  let raw_data = app.get_graph_checkpoints.checkpoints;
3105  let x_targets = raw_data.map((i) => i.time);
3106
3107  let id_left = d3.bisect(x_targets, x_raw) - 1;
3108  let id_right = id_left + 1;
3109  if (id_left == -1) {
3110    id_left = 0;
3111    id_right = 0;
3112  }
3113  if (id_right == x_targets.length) {
3114    id_left = id_right = x_targets.length - 1;
3115  }
3116  let x_left = raw_data[id_left];
3117  let x_right = raw_data[id_right];
3118  let ratio;
3119  try {
3120    ratio = (x_raw - x_left.time) / Math.max(x_right.time - x_left.time, 1);
3121  } catch {
3122    return;
3123  }
3124  const find_y = (left_val, right_val) =>
3125    left_val * (1 - ratio) + right_val * ratio;
3126  const stat = target_stat[gid];
3127  if (stat == undefined) {
3128    return;
3129  }
3130  let expected_y = find_y(x_left.expected[stat], x_right.expected[stat]);
3131  $("#" + stat + "-expected-you").html(expected_y.toFixed(2));
3132  let digits = 2;
3133  if (stat == "points") {
3134    digits = 0;
3135  }
3136  let realized_y = find_y(x_left.realized[stat], x_right.realized[stat]);
3137  $("#" + stat + "-realized-you").html(realized_y.toFixed(digits));
3138
3139  if (!reset) {
3140    let projected_y = find_y(x_left.projected[stat], x_right.projected[stat]);
3141    $("#" + stat + "-projected-you").html(projected_y.toFixed(2));
3142  } else {
3143    let xh = raw_data[raw_data.length - 1]["projected"][stat];
3144    $("#" + stat + "-projected-you").html(xh.toFixed(2));
3145  }
3146
3147  if (stat == "points") {
3148    let expected_y = find_y(x_left.average.expected, x_right.average.expected);
3149    $("#" + stat + "-expected-avg").html(expected_y.toFixed(2));
3150    let realized_y = find_y(x_left.average.realized, x_right.average.realized);
3151    $("#" + stat + "-realized-avg").html(realized_y.toFixed(2));
3152
3153    if (!reset) {
3154      let projected_avg_y = find_y(
3155        x_left.projected["avg"],
3156        x_right.projected["avg"]
3157      );
3158      $("#" + stat + "-projected-avg").html(projected_avg_y.toFixed(2));
3159    } else {
3160      let projected_avg_y = raw_data[raw_data.length - 1]["projected"]["avg"];
3161      $("#" + stat + "-projected-avg").html(projected_avg_y.toFixed(2));
3162    }
3163  }
3164}
3165
3166function synced_enter(e, d) {
3167  let x_now = d3.pointer(e)[0];
3168  let x_raw = axis_functions[d].x.invert(x_now);
3169  const colors = getThemeColors();
3170
3171  $("svg.active-graph").each((i, svg) => {
3172    let gid = svg.id;
3173    let x = axis_functions[gid].x;
3174    let y = axis_functions[gid].y;
3175    let target = d3.select($(svg).find(".svg-actual")[0]);
3176    target
3177      .append("line")
3178      .attr("class", "guide")
3179      .attr("x1", x(x_raw))
3180      .attr("y1", y.range()[0])
3181      .attr("x2", x(x_raw))
3182      .attr("y2", y.range()[1])
3183      .style("stroke", colors.markerLine)
3184      .style("stroke-dasharray", "3,1")
3185      .style("opacity", 0.5)
3186      .attr("pointer-events", "none");
3187    target
3188      .append("text")
3189      .attr("class", "guidetext")
3190      .attr("text-anchor", "middle")
3191      .attr("x", x(x_raw))
3192      .attr("y", -2)
3193      .attr("font-size", "3pt")
3194      .attr("fill", colors.text)
3195      .text(new Date(x_raw).toLocaleString());
3196
3197    if (gid == "timeline") {
3198      return;
3199    }
3200
3201    update_graph_hover_values(x_raw, gid, false);
3202  });
3203}
3204
3205function synced_move(e, d) {
3206  let x_now = d3.pointer(e)[0];
3207  let x_raw = axis_functions[d].x.invert(x_now);
3208  $("svg.active-graph").each((i, svg) => {
3209    let gid = svg.id;
3210    let target = d3.select($(svg).find(".svg-actual")[0]);
3211    let x = axis_functions[gid].x;
3212    let y = axis_functions[gid].y;
3213
3214    target.select(".guide").attr("x1", x(x_raw)).attr("x2", x(x_raw));
3215    target
3216      .select(".guidetext")
3217      .attr("x", x(x_raw))
3218      .text(new Date(x_raw).toLocaleString());
3219
3220    update_graph_hover_values(x_raw, gid, false);
3221  });
3222}
3223
3224function synced_leave() {
3225  $("line.guide").remove();
3226  $("text.guidetext").remove();
3227  reset_graph_values();
3228}
3229
3230function synced_mark(e, d) {
3231  if (app.marked_dt !== undefined) {
3232    app.marked_dt = undefined;
3233    $("line.mark").remove();
3234    $("text.marktext").remove();
3235    reset_graph_values();
3236    return;
3237  }
3238  let x_now = d3.pointer(e)[0];
3239  let x_raw = axis_functions[d].x.invert(x_now);
3240  app.marked_dt = x_raw;
3241  const colors = getThemeColors();
3242  $("svg.active-graph").each((i, svg) => {
3243    let gid = svg.id;
3244    let x = axis_functions[gid].x;
3245    let y = axis_functions[gid].y;
3246    let target = d3.select($(svg).find(".svg-actual")[0]);
3247    target
3248      .append("line")
3249      .attr("class", "mark")
3250      .attr("x1", x(x_raw))
3251      .attr("y1", y.range()[0])
3252      .attr("x2", x(x_raw))
3253      .attr("y2", y.range()[1])
3254      .style("stroke", colors.markerLine)
3255      .style("stroke-dasharray", "3,1")
3256      .style("opacity", 0.7)
3257      .attr("pointer-events", "none");
3258    target
3259      .append("text")
3260      .attr("class", "marktext")
3261      .attr("text-anchor", "middle")
3262      .attr("x", x(x_raw))
3263      .attr("y", -2)
3264      .attr("font-size", "3pt")
3265      .attr("fill", colors.text)
3266      .text(new Date(x_raw).toLocaleString());
3267
3268    if (gid == "timeline") {
3269      return;
3270    }
3271
3272    update_graph_hover_values(x_raw, gid, false);
3273  });
3274}
3275
3276function zoomInSvg() {
3277  let svgEl = document.querySelector("svg#timeline");
3278  let currentWidth = svgEl.getBoundingClientRect().width;
3279  let currentMinWidth = parseInt(svgEl.style.minWidth.replace("px", ""));
3280  if (currentMinWidth < currentWidth) {
3281    svgEl.style.minWidth = currentWidth + 100 + "px";
3282  } else {
3283    if (currentMinWidth < 3000) {
3284      svgEl.style.minWidth = currentMinWidth + 100 + "px";
3285    }
3286  }
3287}
3288
3289function zoomOutSvg() {
3290  let svgEl = document.querySelector("svg#timeline");
3291  let currentMinWidth = parseInt(svgEl.style.minWidth.replace("px", ""));
3292  if (currentMinWidth > 200) {
3293    svgEl.style.minWidth = currentMinWidth - 100 + "px";
3294  }
3295}
3296
3297function refreshFixtureAndRP() {
3298  $("#fixtureModal")
3299    .modal({
3300      backdrop: "static",
3301      keyboard: false,
3302    })
3303    .modal("show");
3304  app.now_dt = new Date().getTime();
3305
3306  Promise.all([load_fixture_data(), load_rp_data()])
3307    .then((values) => {
3308      app.$nextTick(() => {
3309        $("#fixtureModal").modal("hide");
3310        refresh_all_graphs();
3311      });
3312    })
3313    .catch((error) => {
3314      console.error("An error has occured: " + error);
3315      $("#fixtureModal").modal("hide");
3316    });
3317}
3318
3319function refresh_all_graphs() {
3320  if (!app.is_all_ready) {
3321    return;
3322  }
3323
3324  $("#graphModal")
3325    .modal({
3326      backdrop: "static",
3327      keyboard: false,
3328    })
3329    .modal("show");
3330  $(".svg-wrapper").empty();
3331  setTimeout(() => {
3332    app.$nextTick(() => {
3333      $(".svg-wrapper").empty();
3334      init_timeline();
3335      Promise.all([
3336        draw_user_graph({
3337          target: "#graph-wrapper-points",
3338          stat: "points",
3339          title: "Points",
3340        }),
3341        draw_user_graph({
3342          target: "#graph-wrapper-diff",
3343          stat: "diff",
3344          title: "Difference to Average",
3345        }),
3346        draw_user_graph({
3347          target: "#graph-wrapper-gain",
3348          stat: "gain",
3349          title: "Weighted Gain (Owned)",
3350        }),
3351        draw_user_graph({
3352          target: "#graph-wrapper-loss",
3353          stat: "loss",
3354          title: "Weighted Loss (Non-owned)",
3355        }),
3356      ])
3357        .then((values) => {
3358          app.$nextTick(() => {
3359            $("#graphModal").modal("hide");
3360            draw_tier_ownership_graph();
3361          });
3362        })
3363        .catch((error) => {
3364          console.log(error);
3365        });
3366    });
3367  }, 100);
3368}
3369
3370async function app_initialize(clear = true) {
3371  // if (!is_active) {
3372  //     app.gw = `GW${parseInt(app.gw.slice(2))+1}`
3373  // }
3374
3375  $("#updateModal")
3376    .modal({
3377      backdrop: "static",
3378      keyboard: false,
3379    })
3380    .modal("show");
3381
3382  if (clear && app.team_id != -2) {
3383    app.initEmptyData();
3384  }
3385
3386  return Promise.all([
3387    load_fixture_data(),
3388    load_element_data(),
3389    load_xp_data(),
3390    load_rp_data(),
3391    load_sample_data(),
3392  ])
3393    .then((values) => {
3394      // $(".svg-wrapper").empty();
3395      load_team_data().then(() => {
3396        if (app.gw == app.next_gw) {
3397          app.findIdealPicks();
3398        }
3399
3400        $("#updateModal").modal("hide");
3401        setTimeout(() => {
3402          refresh_all_graphs();
3403        }, 100);
3404      });
3405    })
3406    .catch((error) => {
3407      console.error("An error has occured: " + error);
3408      $("#updateModal").modal("hide");
3409    });
3410}
3411
3412$(document).ready(function () {
3413  let url = window.location.search;
3414  const params = new URLSearchParams(url);
3415
3416  Vue.$cookies.config("270d");
3417  app_initialize().then(() => {
3418    if (params.get("team") != null) {
3419      let sorted = params.get("sorted") == 1;
3420      let picks = params
3421        .get("team")
3422        .split(",")
3423        .map((i) => parseInt(i));
3424      let captain = params.get("cap");
3425      let vice_cap = params.get("vc");
3426      let tc = params.get("tc");
3427      let gw = params.get("gw");
3428      app.setTeamWithURL(sorted, picks, captain, vice_cap, tc, gw);
3429    } else {
3430      let cached_team = Vue.$cookies.get(current_season + "_" + "team_id");
3431      if (cached_team !== null) {
3432        app.loadAutoSettings();
3433      } else {
3434        $("#teamModal").modal("show");
3435      }
3436    }
3437
3438    let xp_data = read_xp_storage();
3439    if (xp_data !== undefined) {
3440      app.xp_storage = xp_data;
3441      if (app.use_upload_data) {
3442        app.useNewXP();
3443      }
3444    }
3445  });
3446  $("#editTeamModal").on("hide.bs.modal", (e) => {
3447    refresh_all_graphs();
3448  });
3449  $("#whatifModal").on("hide.bs.modal", (e) => {
3450    refresh_all_graphs();
3451  });
3452  // $("#sourceModal").on('hide.bs.modal', (e) => {
3453  //     refresh_all_graphs();
3454  // });
3455  $("#matchReportModal").on("hide.bs.modal", (e) => {
3456    $("#match_report_all").DataTable().destroy();
3457    app.modal_selected_game = undefined;
3458  });
3459  $("#showTeamModal").on("shown.bs.modal", (e) => {
3460    $("#expected-tab").tab("show");
3461  });
3462  $("#matchReportModal").on("shown.bs.modal", (e) => {
3463    app.$nextTick(() => {
3464      $("#match_report_all").DataTable({
3465        order: [[4, "desc"]],
3466        lengthChange: false,
3467        fixedColumn: true,
3468        pageLength: window.screen.width <= 768 ? 10 : 15,
3469      });
3470    });
3471  });
3472
3473  // Listen for theme toggle button clicks and refresh graphs
3474  $(document).on(
3475    "click",
3476    "[data-theme-toggle], .theme-toggle-btn, #theme-toggle",
3477    function () {
3478      setTimeout(() => {
3479        refresh_all_graphs();
3480      }, 100);
3481    }
3482  );
3483});

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.