PageSourceSearch

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

js fploptimized.com collected 2026-10-04 13:32:11 UTC 192,580 bytes, 6,566 lines download raw bytes

1let season_results = [
2  // [1, 159801, 324.82],
3  // [10, 10322, 289.82],
4  // [100, 3089815, 237.82],
5  // [200, 316872, 221.82],
6  // [500, 3350292, 199.82],
7  // [1000, 251719, 181.82],
8  // [2000, 40030, 161.82],
9  // [5000, 2016845, 131.82],
10  // [10000, 1077954, 106.82],
11  // [20000, 2006, 77.82],
12  // [50000, 1061692, 30.82],
13  // [100000, 11584, -14.18],
14  // [200000, 4820324, -73.18],
15  // [500000, 245679, -180.18],
16  // [1000000, 541485, -286.18],
17  // [1500000, 1207710, -363.18],
18  // [2000000, 1630750, -428.18],
19  // [4000000, 237940, -641.18],
20  // [6000000, 2858219, -840.18],
21  // [9000000, 9092949, -2059.18]
22];
23
24var app = new Vue({
25  el: "#app",
26  errorCaptured: function (err) {
27    debugger;
28    console.log("Caught error", err.message);
29    return false;
30  },
31  data: {
32    season: season,
33    team_id: "",
34    next_gw: gw,
35    off_season: off_season,
36    max_gw: Math.min(gw, 38),
37    points_data: {},
38    eo_data: {},
39    fixture_data: [],
40    fdr_data: [],
41    team_info: {},
42    team_data: {},
43    el_data: [],
44    xp_data: undefined,
45    ready: false,
46    loading: false,
47    top_players_table: undefined,
48    gw_table: undefined,
49    all_picks_table: undefined,
50    rival_info: [],
51    sample_selection: 0,
52    sample_options: [],
53    include_hits: true,
54    show_xp_totals: false,
55    show_diff_totals: false,
56    show_all_season: false,
57    show_all_transfers: false,
58    toty_relative: false,
59    season_targets: season_results,
60    luck_input: 0,
61    maximize: false,
62    show_pts_on_ts: true,
63    show_info_ts: false,
64    use_team_colors: false,
65    // exclude_fh_from_age_0: false
66    // toty_expected: false,
67    caches_enabled: false,
68  },
69  computed: {
70    is_ready() {
71      return (
72        this.ready && !_.isEmpty(this.team_data) && !_.isEmpty(this.points_data)
73      );
74    },
75    completed() {
76      return _.size(this.team_data);
77    },
78    stats_2d() {
79      if (!this.is_ready) {
80        return [];
81      }
82      let structured_data = Object.entries(this.points_data).map((i) =>
83        i[1].map((j) => [i[0], j.id, j.e])
84      );
85      return structured_data.flat();
86    },
87    points_2d() {
88      if (!this.is_ready) {
89        return {};
90      }
91      let st_data = this.stats_2d;
92      let pt_data = st_data.map((e) => {
93        let pts = e[2].map((f) => f.stats.map((s) => s.points)).flat();
94        let tot_pts = pts.reduce((a, b) => a + b, 0);
95        return { gw: e[0], id: e[1], pts: tot_pts };
96      });
97      return pt_data;
98    },
99    gw_pid_pts() {
100      if (!this.is_ready) {
101        return [];
102      }
103      let pts_data = this.points_2d;
104      let gw_grouped = _.groupBy(pts_data, "gw");
105      Object.keys(gw_grouped).map((key, index) => {
106        gw_grouped[key] = Object.fromEntries(
107          gw_grouped[key].map((i) => [i.id, i.pts])
108        );
109      });
110      for (let gw of _.range(1, parseInt(app.next_gw))) {
111        if (gw_grouped[gw] == undefined) {
112          gw_grouped[gw] = {};
113        }
114      }
115      return gw_grouped;
116    },
117    pid_gw_pts() {
118      if (!this.is_ready) {
119        return [];
120      }
121      let pts_data = this.points_2d;
122      let id_grouped = _.groupBy(pts_data, "id");
123      Object.keys(id_grouped).map((key, index) => {
124        id_grouped[key] = Object.fromEntries(
125          id_grouped[key].map((i) => [i.gw, i.pts])
126        );
127      });
128      return id_grouped;
129    },
130    fpl_element() {
131      if (!this.is_ready) {
132        return [];
133      }
134      return Object.fromEntries(this.el_data.map((i) => [i.id, i]));
135    },
136    user_player_stats() {
137      if (!this.is_ready) {
138        return [];
139      }
140      let gw_pid_pts = this.gw_pid_pts;
141      let fpl_data = this.fpl_element;
142      let picks = Object.entries(this.team_data)
143        .map((i) => i[1].picks.map((j) => ({ gw: i[0], ...j })))
144        .flat();
145      picks.forEach((p) => {
146        p["points"] = (gw_pid_pts[p.gw] || {})[p.element] || 0;
147        p["raw"] = fpl_data[p.element];
148      });
149      return picks;
150    },
151    user_player_sum() {
152      if (!this.is_ready) {
153        return [];
154      }
155      let picks = this.user_player_stats;
156      let grouped_pick_data = _(picks).groupBy("element").value();
157      let grouped_all_stats = {};
158      let eo_dict = this.parsed_eo_data.dict;
159      Object.keys(grouped_pick_data).map((key) => {
160        e = grouped_all_stats[key] = {};
161        e["id"] = key;
162        e["squad_count"] = grouped_pick_data[key].length;
163        e["lineup_count"] = grouped_pick_data[key].filter(
164          (i) => i.multiplier > 0
165        ).length;
166        e["points_total"] = grouped_pick_data[key]
167          .map((i) => (i.points || 0) * i.multiplier)
168          .reduce((a, b) => a + b, 0);
169        let pt_total = grouped_pick_data[key]
170          .map(
171            (i) =>
172              (i.points || 0) *
173              (i.multiplier - (eo_dict[i.gw + "_" + i.element] || 0) / 100)
174          )
175          .reduce((a, b) => a + b, 0);
176        e["points_total_rel"] = _.round(pt_total, 1);
177        e["cap_count"] = grouped_pick_data[key].filter(
178          (i) => i.multiplier > 1
179        ).length;
180        e["position"] = grouped_pick_data[key][0].raw.element_type;
181        e["name"] = grouped_pick_data[key][0].raw.web_name;
182        e["raw"] = grouped_pick_data[key][0].raw;
183      });
184      let sorted_data = _.orderBy(
185        grouped_all_stats,
186        ["points_total", "squad_count", "lineup_count"],
187        ["desc", "desc", "desc"]
188      );
189      return sorted_data;
190    },
191    user_player_relative_sum() {
192      if (!this.is_ready) {
193        return [];
194      }
195      let picks = this.user_player_stats;
196      let grouped_pick_data = _(picks).groupBy("element").value();
197      let grouped_all_stats = {};
198      let eo_dict = this.parsed_eo_data.dict;
199      Object.keys(grouped_pick_data).map((key) => {
200        e = grouped_all_stats[key] = {};
201        e["id"] = key;
202        e["squad_count"] = grouped_pick_data[key].length;
203        e["lineup_count"] = grouped_pick_data[key].filter(
204          (i) => i.multiplier > 0
205        ).length;
206        eo_dict;
207        // let eo = eo_dict[]
208        let pt_total = grouped_pick_data[key]
209          .map(
210            (i) =>
211              (i.points || 0) *
212              (i.multiplier - (eo_dict[i.gw + "_" + i.element] || 0) / 100)
213          )
214          .reduce((a, b) => a + b, 0);
215        e["points_total"] = _.round(pt_total, 1);
216        e["cap_count"] = grouped_pick_data[key].filter(
217          (i) => i.multiplier > 1
218        ).length;
219        e["position"] = grouped_pick_data[key][0].raw.element_type;
220        e["name"] = grouped_pick_data[key][0].raw.web_name;
221        e["raw"] = grouped_pick_data[key][0].raw;
222      });
223      let sorted_data = _.orderBy(
224        grouped_all_stats,
225        ["points_total", "squad_count", "lineup_count"],
226        ["desc", "desc", "desc"]
227      );
228      return sorted_data;
229    },
230    toty_final() {
231      if (!this.is_ready) {
232        return [];
233      }
234      if (this.toty_relative) {
235        return this.user_toty_relative;
236      } else {
237        return this.user_toty;
238      }
239    },
240    user_toty() {
241      if (!this.is_ready) {
242        return [];
243      }
244      let player_sum_data = this.user_player_sum;
245
246      let selected_players = [];
247      let lineup_count = { 1: 0, 2: 0, 3: 0, 4: 0 };
248      let squad_count = { 1: 0, 2: 0, 3: 0, 4: 0 };
249      let selected_index = {};
250
251      // Initial append for minimum valid formation
252      for (let pos = 1; pos < 5; pos++) {
253        let min_target = element_type[pos].min;
254        while (lineup_count[pos] < min_target) {
255          for (let i = 0; i < player_sum_data.length; i++) {
256            let p = player_sum_data[i];
257            if (p.position != pos) {
258              continue;
259            }
260            selected_players.push({
261              player: p,
262              lineup: true,
263              position: pos,
264              points_total: p["points_total"],
265              cap_count: p["cap_count"],
266            });
267            lineup_count[pos] += 1;
268            squad_count[pos] += 1;
269            selected_index[p.id] = true;
270            if (lineup_count[pos] >= min_target) {
271              break;
272            }
273          }
274          break;
275        }
276      }
277
278      for (let i = 0; i < player_sum_data.length; i++) {
279        let p = player_sum_data[i];
280        let pos = p["position"];
281        if (selected_index[p.id]) {
282          continue;
283        }
284        if (
285          lineup_count[pos] < element_type[pos].max &&
286          getSum(Object.values(lineup_count)) < 11
287        ) {
288          selected_players.push({
289            player: p,
290            lineup: true,
291            position: pos,
292            points_total: p["points_total"],
293            cap_count: p["cap_count"],
294            is_keeper: pos == 1,
295          });
296          lineup_count[pos] += 1;
297          squad_count[pos] += 1;
298        } else if (squad_count[pos] < element_type[pos].cnt) {
299          selected_players.push({
300            player: p,
301            lineup: false,
302            position: pos,
303            points_total: p["points_total"],
304            cap_count: p["cap_count"],
305            is_keeper: pos == 1,
306          });
307          squad_count[pos] += 1;
308        }
309        if (getSum(Object.values(squad_count)) == 15) {
310          break;
311        }
312      }
313
314      let pos_ctr = { 1: 1, 2: 1, 3: 1, 4: 1, B: 1 };
315      selected_players = _.orderBy(
316        selected_players,
317        ["is_keeper", "points_total"],
318        ["desc", "desc"]
319      );
320      selected_players.forEach((player) => {
321        let pos = player.position;
322        let cnt = lineup_count[pos];
323        if (player.lineup) {
324          player.x = (122 / (cnt + 1)) * pos_ctr[pos] - 17;
325          player.y = (pos - 1) * 35 + 3;
326          pos_ctr[pos] += 1;
327        } else {
328          player.x = (122 / 5) * pos_ctr["B"] - 17;
329          pos_ctr["B"] += 1;
330          player.y = 138.5;
331        }
332      });
333      return selected_players;
334    },
335    user_toty_relative() {
336      if (!this.is_ready) {
337        return [];
338      }
339      let player_sum_data = this.user_player_relative_sum;
340
341      let selected_players = [];
342      let lineup_count = { 1: 0, 2: 0, 3: 0, 4: 0 };
343      let squad_count = { 1: 0, 2: 0, 3: 0, 4: 0 };
344      let selected_index = {};
345
346      // Initial append for minimum valid formation
347      for (let pos = 1; pos < 5; pos++) {
348        let min_target = element_type[pos].min;
349        while (lineup_count[pos] < min_target) {
350          for (let i = 0; i < player_sum_data.length; i++) {
351            let p = player_sum_data[i];
352            if (p.position != pos) {
353              continue;
354            }
355            selected_players.push({
356              player: p,
357              lineup: true,
358              position: pos,
359              points_total: p["points_total"],
360              cap_count: p["cap_count"],
361            });
362            lineup_count[pos] += 1;
363            squad_count[pos] += 1;
364            selected_index[p.id] = true;
365            if (lineup_count[pos] >= min_target) {
366              break;
367            }
368          }
369          break;
370        }
371      }
372
373      for (let i = 0; i < player_sum_data.length; i++) {
374        let p = player_sum_data[i];
375        let pos = p["position"];
376        if (selected_index[p.id]) {
377          continue;
378        }
379        if (
380          lineup_count[pos] < element_type[pos].max &&
381          getSum(Object.values(lineup_count)) < 11
382        ) {
383          selected_players.push({
384            player: p,
385            lineup: true,
386            position: pos,
387            points_total: p["points_total"],
388            cap_count: p["cap_count"],
389            is_keeper: pos == 1,
390          });
391          lineup_count[pos] += 1;
392          squad_count[pos] += 1;
393        } else if (squad_count[pos] < element_type[pos].cnt) {
394          selected_players.push({
395            player: p,
396            lineup: false,
397            position: pos,
398            points_total: p["points_total"],
399            cap_count: p["cap_count"],
400            is_keeper: pos == 1,
401          });
402          squad_count[pos] += 1;
403        }
404        if (getSum(Object.values(squad_count)) == 15) {
405          break;
406        }
407      }
408
409      let pos_ctr = { 1: 1, 2: 1, 3: 1, 4: 1, B: 1 };
410      selected_players = _.orderBy(
411        selected_players,
412        ["is_keeper", "points_total"],
413        ["desc", "desc"]
414      );
415      selected_players.forEach((player) => {
416        let pos = player.position;
417        let cnt = lineup_count[pos];
418        if (player.lineup) {
419          player.x = (122 / (cnt + 1)) * pos_ctr[pos] - 17;
420          player.y = (pos - 1) * 35 + 3;
421          pos_ctr[pos] += 1;
422        } else {
423          player.x = (122 / 5) * pos_ctr["B"] - 17;
424          pos_ctr["B"] += 1;
425          player.y = 138.5;
426        }
427      });
428      return selected_players;
429    },
430    stats_per_gw_detailed() {
431      if (!this.is_ready) {
432        return [];
433      }
434      let stats = this.stats_2d;
435      let all_data = stats
436        .map((i) =>
437          i[2].map((j) =>
438            j.stats.map((k) => ({
439              gw: i[0],
440              id: i[1],
441              fixture: j.fixture,
442              ...k,
443            }))
444          )
445        )
446        .flat()
447        .flat();
448      return all_data;
449    },
450    user_picks_detailed() {
451      if (!this.is_ready) {
452        return [];
453      }
454      let team_data = this.team_data;
455      let fpl_data = this.fpl_element;
456      let stat_detailed = this.stats_per_gw_detailed;
457      let all_team_picks = Object.keys(team_data)
458        .map((i) => team_data[i].picks.map((j) => ({ gw: i, ...j })))
459        .flat();
460      let pick_mult_map = all_team_picks.map((i) => [
461        i.gw + "," + i.element,
462        i.multiplier,
463      ]);
464      let pick_mult = Object.fromEntries(pick_mult_map);
465      let picked_stats = stat_detailed.filter(
466        (i) => i.gw + "," + i.id in pick_mult
467      );
468      picked_stats.forEach((p) => {
469        p.multiplier = pick_mult[p.gw + "," + p.id];
470        p.total_points = p.points * p.multiplier;
471        p.eltype = fpl_data[p.id].element_type;
472        p.name = fpl_data[p.id].web_name;
473      });
474
475      return picked_stats;
476    },
477    user_picks_with_order() {
478      if (!this.is_ready) {
479        return [];
480      }
481      if (_.isEmpty(this.user_ownership_gain_loss)) {
482        return [];
483      }
484      let td = _.cloneDeep(this.team_data);
485      let picks = Object.keys(td)
486        .map((i) => td[i].picks.map((j) => ({ gw: parseInt(i), ...j })))
487        .flat();
488      // _.cloneDeep(this.user_ownership_gain_loss.picked_players)
489      let chips = Object.fromEntries(
490        Object.entries(td).map((i) => [i[0], i[1].active_chip])
491      );
492      let fpl_el = this.fpl_element;
493      let rp_dict = this.pid_gw_pts;
494
495      picks.forEach((el) => {
496        el.element_type = fpl_el[el.element].element_type;
497      });
498      picks.forEach((el) => {
499        el.rp = _.get(rp_dict, el.element + "." + el.gw) || 0;
500      });
501
502      // calculate viz positions
503      let gw_player_dict = {};
504      let gw_order_dict = {};
505      let eltypes = [1, 2, 3, 4, 5];
506      let el_lowest = { 1: 1, 2: 3, 3: 8, 4: 13, 5: 16 };
507      let el_highest = { 1: 2, 2: 7, 3: 12, 4: 15, 5: 16 };
508      // let order = _.range(1,16)
509
510      let gws = _.sortedUniq(picks.map((i) => i.gw)).map((i) => parseInt(i));
511      gws.forEach((gw) => {
512        let gw_picks = picks.filter((i) => i.gw == gw);
513
514        eltypes.forEach((pos) => {
515          let pos_picks = gw_picks.filter((i) => i.element_type == pos);
516          let prev_gw = chips[gw - 1] == "freehit" ? gw - 2 : gw - 1;
517          let prev_picks = picks.filter((i) => i.gw == prev_gw);
518
519          let one_before = parseInt(gw) - 1;
520          let history_picks = picks.filter((i) => i.gw == one_before);
521          let one_after = parseInt(gw) + 1; // chips[gw+1] == 'freehit' ? gw+2 : gw+1
522          let future_picks = picks.filter((i) => i.gw == one_after);
523
524          // existing ones
525          let existing_players = pos_picks.filter((i) =>
526            prev_picks.map((i) => i.element).includes(i.element)
527          );
528          existing_players.forEach((el) => {
529            let previous = _.get(gw_player_dict, prev_gw + "." + el.element);
530
531            let prev = history_picks.find((i) => i.element == el.element);
532            let next = future_picks.find((i) => i.element == el.element);
533
534            el.sort_order = previous;
535            if (prev != undefined) {
536              el.sort_start = prev.sort_start;
537            } else {
538              el.sort_start = gw;
539            }
540            if (next == undefined) {
541              el.sort_finish = gw;
542            }
543            _.set(gw_player_dict, gw + "." + el.element, previous);
544            _.set(gw_order_dict, gw + "." + previous, el.element);
545          });
546          // new players
547          let new_players = _.differenceWith(
548            pos_picks,
549            existing_players,
550            _.isEqual
551          );
552          new_players.forEach((el) => {
553            let start_order = el_lowest[el.element_type];
554            let finish_order = el_highest[el.element_type];
555            let posssible = _.range(start_order, finish_order + 1).filter(
556              (i) => _.get(gw_order_dict, gw + "." + i) == undefined
557            );
558            el.sort_order = posssible[0];
559            el.sort_start = gw;
560            _.set(gw_player_dict, gw + "." + el.element, posssible[0]);
561            _.set(gw_order_dict, gw + "." + posssible[0], el.element);
562
563            let next = future_picks.find((i) => i.element == el.element);
564            if (next == undefined || chips[gw] == "freehit") {
565              el.sort_finish = gw;
566            }
567          });
568        });
569      });
570      return picks;
571    },
572    pick_stats() {
573      if (_.isEmpty(this.user_picks_with_order)) {
574        return {};
575      }
576      let data = this.user_picks_with_order;
577      let blanks = data.filter((i) => i.multiplier > 0 && i.rp <= 3).length;
578      let picked = data.filter((i) => i.multiplier > 0).length;
579      let tens = data.filter((i) => i.multiplier > 0 && i.rp >= 10).length;
580      return { blanks, picked, tens };
581    },
582    user_grouped_by_fixture() {
583      return _(this.user_picks_detailed)
584        .groupBy("id")
585        .map((i, v) => [v, _(i).groupBy("fixture").value()])
586        .value();
587    },
588    user_picks_origin() {
589      if (!this.is_ready) {
590        return [];
591      }
592      let rp_dict = this.pid_gw_pts;
593      data = this.team_data;
594      let gameweeks = Object.keys(data).map((i) => parseInt(i));
595      let wc_count = 1;
596      let fh_count = 1;
597      gameweeks.forEach((gw) => {
598        picks = data[gw].picks;
599        // if GW 1 or WC: then everyone is WC -  reset
600        if (gw == gameweeks[0]) {
601          picks.forEach((p) => {
602            p.gw = gw;
603            p.origin = "initial";
604            p.origin_text = "initial";
605            p.origin_gw = gw;
606            p.age = 0;
607            p.age_group = p.age;
608          });
609        }
610        // 2022-2023 special: World Cup Wildcard
611        // else if (gw == 17) {
612        //     picks.forEach((p) => {
613        //         p.gw = gw
614        //         p.origin = 'wildcard'
615        //         p.origin_text = 'wcb'
616        //         p.origin_gw = gw
617        //         p.age = 0
618        //         p.age_group = p.age
619        //     })
620        // }
621        else if (data[gw].active_chip == "wildcard") {
622          picks.forEach((p) => {
623            p.gw = gw;
624            p.origin = "wildcard";
625            p.origin_text = "wc" + wc_count;
626            p.origin_gw = gw;
627            p.age = 0;
628            p.age_group = p.age;
629            p.wildcard_cnt = wc_count;
630          });
631          wc_count += 1;
632        } else if (data[gw].active_chip == "freehit") {
633          picks.forEach((p) => {
634            p.gw = gw;
635            p.origin = "freehit";
636            p.origin_text = "fh" + fh_count;
637            p.origin_gw = gw;
638            p.age = 0;
639            p.age_group = p.age;
640            p.freehit_cnt = fh_count;
641          });
642          fh_count += 1;
643        } else {
644          // check if last gw was FH
645          let target_gw =
646            data[gw - 1].active_chip == "freehit" ? gw - 2 : gw - 1;
647          last_gw_picks = data[target_gw].picks;
648          picks.forEach((p) => {
649            last_entry = last_gw_picks.find((i) => i.element == p.element);
650            if (last_entry) {
651              p.gw = gw;
652              p.origin = last_entry.origin;
653              p.origin_text = last_entry.origin_text;
654              p.origin_gw = last_entry.origin_gw;
655              p.age = gw - p.origin_gw;
656              p.age_group = p.age > 10 ? "11+" : p.age;
657              p.wildcard_cnt = last_entry.wildcard_cnt;
658            } else {
659              p.gw = gw;
660              p.origin = "transfer";
661              p.origin_text = "transfer";
662              p.origin_gw = gw;
663              p.age = 0;
664              p.age_group = p.age;
665            }
666          });
667        }
668      });
669
670      gameweeks.forEach((gw) => {
671        data[gw].picks.forEach((p) => {
672          p.points = _.get(rp_dict, p.element + "." + gw) || 0;
673          p.eff_points = p.points * p.multiplier;
674        });
675      });
676
677      let all_players = Object.values(data)
678        .map((i) => i.picks)
679        .flat();
680
681      let age_pts = _.map(_.groupBy(all_players, "age"), (val, key) => [
682        key,
683        _.sumBy(val, "eff_points"),
684      ]);
685
686      stats = {
687        total: _.sum(all_players.map((i) => i.eff_points)),
688        pts_main: {
689          initial: _.sum(
690            all_players
691              .filter((i) => i.origin == "initial")
692              .map((i) => i.eff_points)
693          ),
694          wildcard: _.sum(
695            all_players
696              .filter((i) => i.origin == "wildcard")
697              .map((i) => i.eff_points)
698          ),
699          freehit: _.sum(
700            all_players
701              .filter((i) => i.origin == "freehit")
702              .map((i) => i.eff_points)
703          ),
704          transfer: _.sum(
705            all_players
706              .filter((i) => i.origin == "transfer")
707              .map((i) => i.eff_points)
708          ),
709        },
710        // 'split': {
711        //     'total_pts_initial': _.sum(all_players.filter(i => i.real_origin == 'initial').map(i => i.eff_points)),
712        //     'total_pts_wildcard_only': _.sum(all_players.filter(i => i.real_origin == 'wildcard').map(i => i.eff_points)),
713        //     'total_pts_freehit': _.sum(all_players.filter(i => i.real_origin == 'freehit').map(i => i.eff_points)),
714        //     'total_pts_transfer': _.sum(all_players.filter(i => i.real_origin == 'transfer').map(i => i.eff_points)),
715        // },
716        pts_details: {
717          initial: _.sum(
718            all_players
719              .filter((i) => i.origin == "initial")
720              .map((i) => i.eff_points)
721          ),
722          wc1: _.sum(
723            all_players
724              .filter((i) => i.origin == "wildcard" && i.wildcard_cnt == 1)
725              .map((i) => i.eff_points)
726          ),
727          wc2: _.sum(
728            all_players
729              .filter((i) => i.origin == "wildcard" && i.wildcard_cnt == 2)
730              .map((i) => i.eff_points)
731          ),
732          // 'wcb': _.sum(all_players.filter(i => i.origin == 'wildcard_bonus').map(i => i.eff_points)),
733          fh1: _.sum(
734            all_players
735              .filter((i) => i.origin == "freehit" && i.freehit_cnt == 1)
736              .map((i) => i.eff_points)
737          ),
738          // 'fh2': _.sum(all_players.filter(i => i.origin == 'freehit' && i.freehit_cnt == 2).map(i => i.eff_points)),
739          transfer: _.sum(
740            all_players
741              .filter((i) => i.origin == "transfer")
742              .map((i) => i.eff_points)
743          ),
744        },
745        groups: {
746          initial: all_players.filter((i) => i.origin == "initial"),
747          wc1: all_players.filter(
748            (i) => i.origin == "wildcard" && i.wildcard_cnt == 1
749          ),
750          wc2: all_players.filter(
751            (i) => i.origin == "wildcard" && i.wildcard_cnt == 2
752          ),
753          // 'wcb': all_players.filter(i => i.origin == 'wildcard_bonus'),
754          fh1: all_players.filter(
755            (i) => i.origin == "freehit" && i.freehit_cnt == 1
756          ),
757          // 'fh2': all_players.filter(i => i.origin == 'freehit' && i.freehit_cnt == 2),
758          transfer: all_players.filter((i) => i.origin == "transfer"),
759        },
760        age_pts,
761      };
762
763      let group_names = [
764        ["Initial", "initial"],
765        ["WC 1", "wc1"],
766        ["WC 2", "wc2"],
767        // ['WC B', 'wcb'],
768        ["Transfers", "transfer"],
769        ["FH", "fh1"],
770        // ['FH 2', 'fh2']
771      ];
772      // let age_groups = _.range(11).concat(['11+'])
773
774      let main_groups = [
775        ["Initial", "initial", "IN"],
776        ["WC", "wildcard", "WC"],
777        ["Transfers", "transfer", "TR"],
778        ["FH", "freehit", "FH"],
779      ];
780
781      let main_coll = (m = _(all_players)
782        .groupBy("origin")
783        .mapValues((val, key) => _(val).groupBy("age_group").value())
784        .value());
785
786      let main_sum = _(m)
787        .mapValues((val, key) =>
788          _(val)
789            .mapValues((ival, ikey) => _.sumBy(ival, "eff_points"))
790            .value()
791        )
792        .value();
793      let main_cnt = _(m)
794        .mapValues((val, key) =>
795          _(val)
796            .mapValues(
797              (ival, ikey) => ival.filter((i) => i.multiplier > 0).length
798            )
799            .value()
800        )
801        .value();
802
803      let tabular_values = {};
804
805      let nested_collection = (e = _(all_players)
806        .groupBy("origin_text")
807        .mapValues((val, key) => _(val).groupBy("age_group").value())
808        .value());
809      let age_groups = _.uniq(
810        _(e)
811          .map((val, key) =>
812            _(val)
813              .map((ival, ikey) => ikey)
814              .value()
815          )
816          .value()
817          .flat()
818      );
819
820      let nested_sum = _(e)
821        .mapValues((val, key) =>
822          _(val)
823            .mapValues((ival, ikey) => _.sumBy(ival, "eff_points"))
824            .value()
825        )
826        .value();
827      let nested_count = _(e)
828        .mapValues((val, key) =>
829          _(val)
830            .mapValues(
831              (ival, ikey) => ival.filter((i) => i.multiplier > 0).length
832            )
833            .value()
834        )
835        .value();
836
837      let age_sums = _(all_players)
838        .groupBy("age_group")
839        .mapValues((val, key) => _(val).sumBy("eff_points"))
840        .value();
841      let age_cnt = _(all_players)
842        .groupBy("age_group")
843        .mapValues((val, key) => val.filter((i) => i.multiplier > 0).length)
844        .value();
845
846      let grp_cnt = _(all_players)
847        .groupBy("origin_text")
848        .mapValues((val, key) => val.filter((i) => i.multiplier > 0).length)
849        .value();
850
851      // group_names.forEach((gr) => {
852      //     age_groups.forEach((age) => {
853      //         if (age.includes('+')) {
854
855      //         }
856      //         else {
857      //             total_in_cell = _.sum(all_players.filter(i => i.origin == gr[1] && i.age == age).map(i => i.eff_points))
858
859      //         }
860      //     })
861      // })
862
863      return Object.freeze({
864        data: data,
865        all_players,
866        stats: stats,
867        group_names,
868        age_groups,
869        nested_sum,
870        nested_count,
871        age_sums,
872        age_cnt,
873        grp_cnt,
874        main_sum,
875        main_cnt,
876        main_groups,
877      });
878    },
879    user_picks_custom_stats() {
880      if (!this.is_ready) {
881        return [];
882      }
883      // let picked_stats = this.user_picks_detailed
884      let team_data = this.team_data;
885      return get_team_stats_picks(team_data);
886    },
887    stats_per_gw() {
888      if (!this.is_ready) {
889        return [];
890      }
891      let stats = this.stats_2d;
892      let gw_data = [];
893      gw_data = [];
894      stats.forEach((e) => {
895        let f = e[2].map((i) => i.stats).flat();
896        let summarized = _(f)
897          .groupBy("identifier")
898          .map((arr, key) => ({
899            key: key,
900            value: arr.map((i) => i.value).reduce((a, b) => a + b, 0),
901            points: arr.map((i) => i.points).reduce((a, b) => a + b, 0),
902          }))
903          .value();
904        for (let st of summarized) {
905          gw_data.push({
906            gw: e[0],
907            id: e[1],
908            ...st,
909          });
910        }
911      });
912      return gw_data;
913    },
914    user_stats_per_gw() {
915      if (!this.is_ready) {
916        return {};
917      }
918      let stats = this.stats_per_gw;
919      let stat_dict = Object.fromEntries(
920        stats.map((i) => [[i.key, i.gw, i.id].join("_"), i])
921      );
922      // let picks = this.team_data.map(i => i.picks)
923      let user_picks = this.user_player_stats;
924      let fpl_data = this.fpl_element;
925      user_picks = user_picks
926        .map((i) => ({ gw: i.gw, id: i.element, multiplier: i.multiplier }))
927        .filter((i) => i.multiplier > 0);
928      let stat_types = Object.keys(player_stat_types);
929
930      let picked_stats = [];
931      for (let st of stat_types) {
932        for (let pick of user_picks) {
933          // let m = stats.find(i => i.key == st && i.gw == pick.gw && i.id == pick.id)
934          let m = stat_dict[[st, pick.gw, pick.id].join("_")];
935          if (m != undefined) {
936            picked_stats.push({
937              ...m,
938              multiplier: pick.multiplier,
939              total_points: pick.multiplier * m.points,
940              eltype: fpl_data[pick.id].element_type,
941            });
942          }
943        }
944      }
945
946      let total_non_bench = user_picks.filter((i) => i.multiplier > 0);
947
948      let gw_grouped = Object.fromEntries(
949        _(picked_stats)
950          .groupBy("gw")
951          .map((i, k) => [k, _.groupBy(i, "key")])
952          .value()
953      );
954
955      let type_grouped = _(picked_stats).groupBy("key").value();
956
957      let pos_grouped = _(picked_stats).groupBy("eltype").value();
958
959      let pt_data = Object.entries(this.team_data).map((i) => ({
960        gw: i[0],
961        net_pts:
962          i[1].entry_history.points - i[1].entry_history.event_transfers_cost,
963        pts: i[1].entry_history.points,
964        hit_pts: i[1].entry_history.event_transfers_cost,
965      }));
966      for (let pgw of pt_data) {
967        let gw = pgw.gw;
968        let type_sums = {};
969        let type_names = {};
970        for (let key in gw_grouped[gw]) {
971          let t = gw_grouped[gw][key];
972          // for defensive contribution the value is the CBIT action count, so
973          // count the number of returns instead of summing the raw actions
974          type_sums[key] =
975            key == "defensive_contribution"
976              ? t.length
977              : getSum(t.map((i) => i.value));
978          type_names[key] = t
979            .map(
980              (ev) =>
981                this.fpl_element[ev.id].web_name +
982                (ev.value > 1 ? ` (${ev.value})` : "")
983            )
984            .join(", ");
985        }
986        pgw.info = { type_sums, type_names };
987      }
988
989      let overall_total = getSum(pt_data.map((i) => i.net_pts));
990
991      return {
992        all_stats: picked_stats,
993        gw_grouped: gw_grouped,
994        type_grouped: type_grouped,
995        points: pt_data,
996        total_picks: total_non_bench.length,
997        pos_grouped: pos_grouped,
998        overall_total,
999      };
1000    },
1001    user_player_gws() {
1002      if (!this.is_ready) {
1003        return [];
1004      }
1005      let team = this.team_data;
1006      let all_picks = Object.entries(team)
1007        .map((i) =>
1008          i[1].picks.map((j) => ({
1009            gw: i[0],
1010            id: j.element,
1011            multiplier: j.multiplier,
1012          }))
1013        )
1014        .flat();
1015      return all_picks;
1016    },
1017    user_points_by_eltype() {
1018      if (!this.is_ready) {
1019        return [];
1020      }
1021      let stats = this.user_player_stats;
1022      let values = stats.map((i) => ({
1023        id: i.element,
1024        eltype: i.raw.element_type,
1025        cost: i.raw.now_cost,
1026        total_points: (i.points || 0) * i.multiplier,
1027      }));
1028      // values = _(values).groupBy('eltype').value()
1029      let price_categories = {
1030        1: [
1031          [50, "Budget"],
1032          [55, "Mid-Price"],
1033          [150, "Premium"],
1034        ],
1035        2: [
1036          [50, "Budget"],
1037          [60, "Mid-Price"],
1038          [150, "Premium"],
1039        ],
1040        3: [
1041          [75, "Budget"],
1042          [100, "Mid-Price"],
1043          [150, "Premium"],
1044        ],
1045        4: [
1046          [75, "Budget"],
1047          [100, "Mid-Price"],
1048          [150, "Premium"],
1049        ],
1050      };
1051      let grouped_vals = {
1052        1: { Budget: 0, "Mid-Price": 0, Premium: 0 },
1053        2: { Budget: 0, "Mid-Price": 0, Premium: 0 },
1054        3: { Budget: 0, "Mid-Price": 0, Premium: 0 },
1055        4: { Budget: 0, "Mid-Price": 0, Premium: 0 },
1056      };
1057      values.forEach((v) => {
1058        let type = v.eltype;
1059        // skip element types without a price category (e.g. managers)
1060        if (!price_categories[type]) {
1061          return;
1062        }
1063        let price_cat = "";
1064        for (let c of price_categories[type]) {
1065          if (v.cost < c[0]) {
1066            price_cat = c[1];
1067            break;
1068          }
1069        }
1070        grouped_vals[type][price_cat] += parseInt(v.total_points);
1071      });
1072
1073      let arr_vals = Object.keys(grouped_vals)
1074        .map((eltype) =>
1075          Object.keys(grouped_vals[eltype]).map((price_cat) => [
1076            element_type[eltype].short,
1077            price_cat,
1078            grouped_vals[eltype][price_cat],
1079          ])
1080        )
1081        .flat();
1082
1083      return arr_vals;
1084    },
1085    user_formation_analysis_by_eltype() {
1086      if (!this.is_ready) {
1087        return [];
1088      }
1089      let stats = this.user_picks_detailed;
1090      let gw_count = _.uniq(stats.map((i) => i.gw)).length;
1091      stats = stats.filter((i) => i.multiplier > 0);
1092      picked = {};
1093      for (let p in element_type) {
1094        let row = (picked[p] = {});
1095        let pos_select = stats.filter((i) => i.eltype == p);
1096        row["picked"] = _.uniqWith(
1097          pos_select,
1098          (a, b) => a.gw == b.gw && a.id == b.id
1099        ).length;
1100        row["games"] = _.uniqWith(
1101          pos_select,
1102          (a, b) => a.id == b.id && a.fixture == b.fixture
1103        ).length;
1104        row["points"] = getSum(pos_select.map((i) => i.total_points));
1105        row["ppp"] = row["points"] / row["picked"];
1106        row["ppg"] = row["points"] / row["games"];
1107        row["avg_pick"] = row["picked"] / gw_count;
1108      }
1109      return picked;
1110    },
1111    user_formation_analysis_by_gw() {
1112      if (!this.is_ready) {
1113        return [];
1114      }
1115      let stats = this.user_picks_detailed;
1116      // let gw_count = _.uniq(stats.map(i => i.gw)).length
1117      let gameweeks = stats.map((i) => i.gw);
1118      stats = stats.filter((i) => i.multiplier > 0);
1119      gw_vals = {};
1120      for (let gw of gameweeks) {
1121        let row = (gw_vals[gw] = {});
1122        let gw_picks = stats.filter(
1123          (i) => i.identifier == "minutes" && i.gw == gw
1124        );
1125        let filtered_picks = _.uniqBy(gw_picks, "id");
1126        let cnt = (row["counts"] = _.countBy(filtered_picks, "eltype"));
1127        row["formation"] =
1128          (filtered_picks.length > 11 ? "(BB) " : "") +
1129          cnt[2] +
1130          "-" +
1131          cnt[3] +
1132          "-" +
1133          cnt[4];
1134        row["points"] = getSum(
1135          stats.filter((i) => i.gw == gw).map((i) => i.total_points)
1136        );
1137        row["total"] = filtered_picks.length;
1138      }
1139      let summary = Object.values(gw_vals);
1140      summary = summary.filter((i) => i.total >= 11);
1141      summary = _.groupBy(summary, "formation");
1142      let sorted_vals = [];
1143      for (let f in summary) {
1144        let pts = getSum(summary[f].map((i) => i.points));
1145        sorted_vals.push({
1146          formation: f,
1147          count: summary[f].length,
1148          points: pts,
1149          avg: pts / summary[f].length,
1150        });
1151      }
1152      sorted_vals = _.orderBy(
1153        sorted_vals,
1154        ["count", "points"],
1155        ["desc", "desc"]
1156      );
1157      return { sorted: sorted_vals, summary: summary, details: gw_vals };
1158    },
1159    parsed_eo_data() {
1160      let eo_data = this.eo_data;
1161      let key = this.sample_options[this.sample_selection];
1162      let gws = Object.keys(eo_data);
1163      let values = [];
1164      let hits = [];
1165      for (let gw of Object.keys(eo_data)) {
1166        if (key in eo_data[gw]) {
1167          let v = Object.keys(eo_data[gw][key])
1168            .filter((i) => i != "meta")
1169            .map((pid) => [gw, pid, eo_data[gw][key][pid].eo]);
1170          values.push(v);
1171          let h = eo_data[gw][key]["meta"];
1172          hits.push(h);
1173        } else {
1174          let v = Object.keys(eo_data[gw]["Overall"])
1175            .filter((i) => i != "meta")
1176            .map((pid) => [gw, pid, eo_data[gw]["Overall"][pid].eo]);
1177          values.push(v);
1178          let h = eo_data[gw]["Overall"]["meta"];
1179          hits.push(h);
1180        }
1181      }
1182      values = values.flat();
1183      // let values = Object.keys(eo_data).map(gw => Object.keys(eo_data[gw][key]).map(pid => [gw, pid, eo_data[gw][key][pid].eo])).flat()
1184      let values_dict = Object.fromEntries(
1185        values.map((i) => [i[0] + "_" + i[1], i[2]])
1186      );
1187      return { gw: gws, raw_values: values, dict: values_dict, hits: hits };
1188    },
1189    user_ownership_gain_loss() {
1190      if (!this.is_ready) {
1191        return {};
1192      }
1193
1194      let ss = this.sample_selection;
1195      let so = this.sample_options[this.sample_selection];
1196
1197      let xp_dict = this.player_xp_dict;
1198
1199      let gws = this.parsed_eo_data.gw;
1200      let all_pids = Object.keys(this.fpl_element);
1201      let all_pairs = Object.fromEntries(
1202        gws
1203          .map((gw) =>
1204            all_pids.map((pid) => [gw + "_" + pid, { gw: gw, id: pid }])
1205          )
1206          .flat()
1207      );
1208      let eo_data = _.cloneDeep(this.parsed_eo_data.dict);
1209      let hits = this.parsed_eo_data.hits;
1210      let user_picks = this.user_player_gws.filter((i) => i.multiplier > 0);
1211      let user_team = this.team_data;
1212      let points = this.gw_pid_pts;
1213      for (let pick of user_picks) {
1214        pick.pts = points?.[pick.gw]?.[pick.id] || 0;
1215        pick.xpts = xp_dict?.[pick.gw + "_" + pick.id] || 0;
1216        pick.total_pts = pick.pts * pick.multiplier;
1217        pick.eo = eo_data[pick.gw + "_" + pick.id] || 0;
1218        pick.owned = true;
1219        pick.own_rate = pick.multiplier * 100 - pick.eo;
1220        pick.net = pick.pts * ((100 * pick.multiplier - pick.eo) / 100);
1221        // table values
1222        pick.points_gain = pick.pts * pick.multiplier;
1223        pick.relative_gain = pick.net;
1224        pick.pred_gain = pick.xpts * ((100 * pick.multiplier - pick.eo) / 100);
1225        pick.points_loss = (pick.pts * pick.eo) / 100;
1226        pick.relative_loss = 0;
1227        pick.pred_loss = 0;
1228        pick.exp_diff = pick.pred_gain;
1229        pick.real_diff = pick.relative_gain;
1230        pick.luck = pick.real_diff - pick.exp_diff;
1231        delete all_pairs[pick.gw + "_" + pick.id];
1232      }
1233      all_pairs = Object.values(all_pairs);
1234      for (let pair of all_pairs) {
1235        pair.pts = points[pair.gw][pair.id] || 0;
1236        pair.xpts = xp_dict[pair.gw + "_" + pair.id] || 0;
1237        pair.total_pts = pair.pts;
1238        pair.eo = eo_data[pair.gw + "_" + pair.id] || 0;
1239        pair.owned = false;
1240        pair.net = -1 * pair.pts * (pair.eo / 100);
1241        // table values
1242        pair.points_gain = 0;
1243        pair.relative_gain = 0;
1244        pair.pred_gain = 0;
1245        pair.points_loss = (pair.pts * pair.eo) / 100;
1246        pair.relative_loss = pair.points_loss;
1247        pair.pred_loss = (pair.xpts * pair.eo) / 100;
1248        pair.exp_diff = -pair.points_loss;
1249        pair.real_diff = -pair.relative_loss;
1250        pair.luck = pair.real_diff - pair.exp_diff;
1251      }
1252      let all_data = all_pairs.concat(user_picks);
1253      let player_gains = _(all_data)
1254        .groupBy("id")
1255        .map((i, v) => {
1256          let gain = getSum(i.map((j) => j.relative_gain));
1257          let loss = getSum(i.map((j) => j.relative_loss));
1258          let pts = getSum(i.map((j) => j.pts));
1259          let xgain = getSum(i.map((j) => j.pred_gain));
1260          let xloss = getSum(i.map((j) => j.pred_loss));
1261          let xpts = getSum(i.map((j) => j.xpts));
1262          let xdiff = xgain - xloss;
1263          let diff = gain - loss;
1264          let luck = diff - xdiff;
1265          return {
1266            id: v,
1267            gain,
1268            loss,
1269            net: gain - loss,
1270            magnitude: Math.abs(gain - loss),
1271            points: pts,
1272            xgain,
1273            xloss,
1274            xnet: xgain - xloss,
1275            xmagnitude: Math.abs(xgain - xloss),
1276            xpoints: xpts,
1277            diff,
1278            xdiff,
1279            luck,
1280            luckmagnitude: Math.abs(luck),
1281          };
1282        })
1283        .value();
1284      player_gains = _(player_gains).orderBy(["magnitude"], ["desc"]).value();
1285      let max_impact = player_gains[0].magnitude;
1286      player_gains.forEach((e) => {
1287        e.impact = (e.magnitude / max_impact) * 100;
1288      });
1289
1290      player_xgains = _(_.cloneDeep(player_gains))
1291        .orderBy(["xmagnitude"], ["desc"])
1292        .value();
1293      let exp_max_impact = player_xgains[0].xmagnitude;
1294      player_xgains.forEach((e) => {
1295        e.ximpact = (e.xmagnitude / exp_max_impact) * 100;
1296      });
1297
1298      player_luck = _(_.cloneDeep(player_gains))
1299        .orderBy(["luckmagnitude"], ["desc"])
1300        .value();
1301      let max_luck = player_luck[0].luckmagnitude;
1302      player_luck.forEach((e) => {
1303        e.luckimpact = (e.luckmagnitude / max_luck) * 100;
1304      });
1305
1306      let combined = Object.entries(_.groupBy(all_data, "gw"));
1307      for (let gw_entry of combined) {
1308        let groups = _.groupBy(gw_entry[1], (entry) => entry.net > 0);
1309        if (!groups.hasOwnProperty("false")) {
1310          groups["false"] = [];
1311        }
1312        if (!groups.hasOwnProperty("true")) {
1313          groups["true"] = [];
1314        }
1315        let loss = getSum(groups.false.map((i) => i.net));
1316        let gain = getSum(groups.true.map((i) => i.net));
1317        let xloss = getSum(groups.false.map((i) => i.xnet));
1318        let xgain = getSum(groups.true.map((i) => i.xnet));
1319        let gw_field_hits = hits[parseInt(gw_entry[0]) - 1];
1320        if (gw_field_hits == undefined) {
1321          gw_field_hits = { hit_total: 0, teams: 0 };
1322        }
1323        let hit_gain =
1324          gw_field_hits.hit_total / Math.max(1, gw_field_hits.teams);
1325        let gain_with_hit = gain + hit_gain;
1326        let hit_loss =
1327          _.get(user_team[gw_entry[0]], "entry_history.event_transfers_cost") ||
1328          0;
1329        let loss_with_hit = loss - hit_loss;
1330        let top_losses = _.orderBy(groups.false, ["net"], ["asc"]).slice(0, 3);
1331        let top_gains = _.orderBy(groups.true, ["net"], ["desc"]).slice(0, 3);
1332        let gw_picks = gw_entry[1].filter((i) => i.owned);
1333        gw = gw_entry[0];
1334        let this_gw_own = gw_picks.map((i) => i.own_rate);
1335        // own_perc = getSum(gw_entry[1].filter(i => i.owned).map(i => i.multiplier*100 - i.eo))/1200
1336        let total_own = getSum(gw_entry[1].map((i) => i.eo));
1337        let risk = (getSum(this_gw_own) / total_own) * 100;
1338        let top_bets_for = _.orderBy(gw_picks, ["own_rate"], ["desc"]).slice(
1339          0,
1340          3
1341        );
1342        let top_bets_against = _.orderBy(
1343          gw_entry[1].filter((i) => !i.owned),
1344          ["eo"],
1345          ["desc"]
1346        ).slice(0, 3);
1347        let net = gain + loss;
1348        let xnet = xgain + xloss;
1349        gw_entry[1] = {
1350          net,
1351          xnet,
1352          loss,
1353          gain,
1354          xgain,
1355          loss_with_hit,
1356          gain_with_hit,
1357          top_gains,
1358          top_losses,
1359          gw,
1360          risk,
1361          this_gw_own,
1362          top_bets_for,
1363          top_bets_against,
1364        };
1365      }
1366      all_pairs = all_pairs.filter(
1367        (i) => i.eo > 2 && i.total_pts > 3 && i.net <= -1
1368      );
1369      return {
1370        picked_players: user_picks,
1371        gains: user_picks,
1372        losses: all_pairs,
1373        combined_per_gw: combined,
1374        combined_per_player: player_gains,
1375        combined_per_player_exp: player_xgains,
1376        comb_per_player_luck: player_luck,
1377      };
1378    },
1379    user_candlestick_values() {
1380      if (!this.is_ready) {
1381        return [];
1382      }
1383      let values = this.user_ownership_gain_loss.combined_per_gw;
1384      let flat_vals = values.map((i) => i[1]).flat();
1385      let current = 0;
1386      let chart_entries = [];
1387      for (let e of flat_vals) {
1388        let original = current;
1389        current += e.gain;
1390        chart_entries.push({ ...e, was: original, current: current, part: 1 });
1391        original = current;
1392        current += e.loss;
1393        chart_entries.push({ ...e, was: original, current: current, part: 2 });
1394      }
1395      return chart_entries;
1396    },
1397    team_fdr_values() {
1398      if (_.isEmpty(this.fdr_data)) {
1399        return [];
1400      }
1401      let fivethirtyeight_data = this.fdr_data;
1402      let teams = _.cloneDeep(teams_ordered);
1403      teams.forEach((team, order) => {
1404        let team_name = team.name;
1405        let entry = fivethirtyeight_data.find((i) => i.name == team_name);
1406        if (entry == undefined) {
1407          let team_long = team.long;
1408          entry = fivethirtyeight_data.find((i) => i.name == team_long);
1409        }
1410        team.offense = parseFloat(entry.off);
1411        team.defense = parseFloat(entry.def);
1412        team.strength = parseFloat(entry.off) - parseFloat(entry.def);
1413        team.raw = entry;
1414        let keys = Object.keys(team_codes);
1415        let key = keys.find((i) => team_codes[i].name == team_name);
1416        team.key = key;
1417        team.order = order;
1418      });
1419
1420      let offense_max = Math.max(...teams.map((i) => i.offense));
1421      let offense_min = Math.min(...teams.map((i) => i.offense));
1422      let defense_max = Math.max(...teams.map((i) => i.defense));
1423      let defense_min = Math.min(...teams.map((i) => i.defense));
1424      let c = (r) =>
1425        d3
1426          .scaleLinear()
1427          .domain([0, 0.5, 1])
1428          .range(["#67ADB2", "#999898", "#C85454"])(r);
1429      teams.forEach((team, order) => {
1430        team.offense_ratio =
1431          (team.offense - offense_min) / (offense_max - offense_min);
1432        team.defense_ratio =
1433          (defense_max - team.defense) / (defense_max - defense_min);
1434        team.offense_color = c(team.offense_ratio);
1435        team.defense_color = c(team.defense_ratio);
1436      });
1437      return teams;
1438    },
1439    user_fdr_values() {
1440      if (!this.is_ready) {
1441        return [];
1442      }
1443      let fixture = Object.fromEntries(this.fixture_data.map((i) => [i.id, i]));
1444      let picks = this.user_picks_detailed.filter(
1445        (i) => i.identifier == "minutes" && i.multiplier > 0
1446      );
1447      let fdr_values = this.team_fdr_values;
1448      if (_.isEmpty(fdr_values)) {
1449        return [];
1450      }
1451      let fpl_data = this.fpl_element;
1452      let total_points_dict = Object.fromEntries(this.user_grouped_by_fixture);
1453
1454      picks.forEach((p) => {
1455        let game = fixture[p.fixture];
1456        let self_team = fpl_data[p.id].team;
1457        let opposite = self_team == game.team_h ? game.team_a : game.team_h;
1458        let fixture_fdr = fdr_values[opposite - 1];
1459        p.fixture_fdr = fixture_fdr;
1460        let pf = (p.player_fdr =
1461          p.eltype <= 2 ? fixture_fdr.offense : fixture_fdr.defense);
1462
1463        p.player_fdr_ratio =
1464          p.eltype <= 2 ? fixture_fdr.offense_ratio : fixture_fdr.defense_ratio;
1465
1466        p.self_team = fdr_values[self_team - 1];
1467        p.opp_team = fdr_values[opposite - 1];
1468        p.position = element_type[p.eltype].short;
1469
1470        p.self_team_fdr =
1471          p.eltype <= 2 ? p.self_team.defense_ratio : p.self_team.offense_ratio;
1472
1473        p.total = getSum(
1474          total_points_dict[p.id][p.fixture].map((i) => i.points)
1475        );
1476      });
1477
1478      picks = _(picks)
1479        .orderBy(
1480          ["player_fdr_ratio", "self_team_fdr", "total"],
1481          ["asc", "desc", "desc"]
1482        )
1483        .value();
1484
1485      return picks;
1486    },
1487    user_fdr_tiers() {
1488      if (!this.is_ready) {
1489        return [];
1490      }
1491      let groups = [0.25, 0.5, 0.75, 1];
1492      let picks = this.user_fdr_values;
1493      picks.forEach((p) => {
1494        for (let i in groups) {
1495          if (p.player_fdr_ratio <= groups[i]) {
1496            p.tier = i;
1497            break;
1498          }
1499        }
1500      });
1501      let tiers = {};
1502      for (let type of [1, 2, 3, 4]) {
1503        let ct = (tiers[type] = {});
1504        let group_picks = picks.filter((i) => i.eltype == type);
1505        for (let tier in _.range(groups.length)) {
1506          ct[tier] = group_picks.filter((i) => i.tier == tier).length;
1507        }
1508      }
1509      return { groups, tiers };
1510    },
1511    player_xp_dict() {
1512      if (_.isEmpty(this.xp_data)) {
1513        return;
1514      }
1515      let e = this.xp_data;
1516      return Object.fromEntries(
1517        e.map((i) => [i.gw + "_" + i.ID, _.round(parseFloat(i.xp), 2)])
1518      );
1519    },
1520    player_gw_reference() {
1521      if (_.isEmpty(this.xp_data)) {
1522        return;
1523      }
1524      let e = this.xp_data;
1525      return Object.fromEntries(e.map((i) => [i.gw + "_" + i.ID, i]));
1526    },
1527    user_gw_results() {
1528      if (_.isEmpty(this.team_data) || _.isEmpty(this.xp_data)) {
1529        return;
1530      }
1531      let xp_dict = this.player_xp_dict;
1532      let rp_dict = this.pid_gw_pts;
1533      let eo_dict = this.parsed_eo_data;
1534      let el_ids = _.uniq(this.el_data.map((i) => i.id));
1535      let gws = Object.keys(this.team_data).map((i) => parseInt(i));
1536      let gw_results = {};
1537      let total_exp_diff = 0,
1538        total_real_diff = 0,
1539        total_xp = 0,
1540        total_rp = 0,
1541        total_field_xp = 0,
1542        total_field_rp = 0,
1543        total_exp_real_diff = 0;
1544
1545      gws.forEach((gw) => {
1546        let gw_picks = this.team_data[gw].picks;
1547        let gw_hit = this.include_hits
1548          ? _.get(this.team_data[gw], "entry_history.event_transfers_cost") || 0
1549          : 0;
1550        let gw_field_hits = eo_dict.hits[gw - 1];
1551        if (gw_field_hits == undefined) {
1552          gw_field_hits = { hit_total: 0, teams: 0 };
1553        }
1554        let field_hit = this.include_hits
1555          ? gw_field_hits.hit_total / Math.max(1, gw_field_hits.teams)
1556          : 0;
1557
1558        let xp =
1559          _.round(
1560            _.sum(
1561              gw_picks.map(
1562                (i) =>
1563                  i.multiplier * (_.get(xp_dict, gw + "_" + i.element) || 0)
1564              )
1565            ),
1566            2
1567          ) - gw_hit;
1568        let rp =
1569          _.sum(
1570            gw_picks.map(
1571              (i) => i.multiplier * (_.get(rp_dict, i.element + "." + gw) || 0)
1572            )
1573          ) - gw_hit;
1574        let diff = _.round(rp - xp, 2);
1575
1576        let field_xp =
1577          _.round(
1578            _.sum(
1579              el_ids.map(
1580                (i) =>
1581                  ((_.get(eo_dict, "dict." + gw + "_" + i) || 0) / 100) *
1582                  (_.get(xp_dict, gw + "_" + i) || 0)
1583              )
1584            ),
1585            2
1586          ) - field_hit;
1587        let field_rp =
1588          _.round(
1589            _.sum(
1590              el_ids.map(
1591                (i) =>
1592                  ((_.get(eo_dict, "dict." + gw + "_" + i) || 0) / 100) *
1593                  (_.get(rp_dict, i + "." + gw) || 0)
1594              )
1595            ),
1596            2
1597          ) - field_hit;
1598        let field_diff = _.round(field_rp - field_xp, 2);
1599
1600        let exp_diff = _.round(xp - field_xp, 2);
1601        let real_diff = _.round(rp - field_rp, 2);
1602
1603        total_exp_diff += exp_diff;
1604        total_real_diff += real_diff;
1605
1606        total_xp += xp;
1607        total_rp += rp;
1608        total_field_xp += field_xp;
1609        total_field_rp += field_rp;
1610
1611        let luck = real_diff - exp_diff;
1612        total_exp_real_diff += luck;
1613
1614        let skill_ratio = _.round(
1615          (100 * exp_diff) / (Math.abs(exp_diff) + Math.abs(luck)),
1616          1
1617        );
1618        let luck_ratio = _.round(
1619          (100 * luck) / (Math.abs(exp_diff) + Math.abs(luck)),
1620          1
1621        );
1622
1623        let total_skill_ratio = _.round(
1624          (100 * total_exp_diff) /
1625            (Math.abs(total_exp_diff) + Math.abs(total_exp_real_diff)),
1626          1
1627        );
1628        let total_luck_ratio = _.round(
1629          (100 * total_exp_real_diff) /
1630            (Math.abs(total_exp_diff) + Math.abs(total_exp_real_diff)),
1631          1
1632        );
1633
1634        gw_results[gw] = {
1635          gw,
1636          xp,
1637          rp,
1638          diff,
1639          field_xp,
1640          field_rp,
1641          field_diff,
1642          exp_diff,
1643          real_diff,
1644          total_exp_diff,
1645          total_real_diff,
1646          total_xp,
1647          total_rp,
1648          total_field_xp,
1649          total_field_rp,
1650          luck,
1651          total_exp_real_diff,
1652          skill_ratio,
1653          luck_ratio,
1654          total_skill_ratio,
1655          total_luck_ratio,
1656        };
1657      });
1658
1659      setTimeout(() => {
1660        if (gw_results[38] != undefined) {
1661          app.luck_input = gw_results[38].total_luck_ratio;
1662        }
1663      }, 100);
1664
1665      return { start: _.min(gws), finish: _.max(gws), data: gw_results };
1666    },
1667    calculated_luck() {
1668      if (_.isEmpty(this.final_outcome)) {
1669        return undefined;
1670      }
1671      let skill = this.final_outcome.total_exp_diff;
1672      if (Math.abs(skill) < 1) {
1673        skill = 1;
1674      }
1675
1676      if (this.luck_input == 0) {
1677        return 0;
1678      } else if (this.luck_input > 0) {
1679        return (Math.abs(skill) * this.luck_input) / (100 - this.luck_input);
1680      } else {
1681        return (Math.abs(skill) * this.luck_input) / (100 + this.luck_input);
1682      }
1683    },
1684    calculated_total() {
1685      if (_.isEmpty(this.final_outcome)) {
1686        return undefined;
1687      }
1688      return this.final_outcome.total_exp_diff + this.calculated_luck;
1689    },
1690    final_outcome() {
1691      if (_.isEmpty(this.user_gw_results)) {
1692        return undefined;
1693      }
1694      return this.user_gw_results.data[38];
1695    },
1696    potential_result() {
1697      let score = this.calculated_total;
1698      for (tier of this.season_targets) {
1699        if (score > tier[2]) {
1700          if (tier[0] == 1) {
1701            return "1";
1702          } else {
1703            return "Top " + formatnumber(tier[0]);
1704          }
1705        }
1706      }
1707      return "9M+";
1708    },
1709    transfer_analysis() {
1710      if (!this.is_ready) {
1711        return undefined;
1712      }
1713      let team_data = this.team_data;
1714      let all_gws = _.range(1, 39);
1715      let analysis = { gws: {} };
1716      let elements = this.fpl_element;
1717      let el_data = this.el_data;
1718      let gw_ref = this.player_gw_reference;
1719      let xp_dict = app.player_xp_dict;
1720      let rp_dict = app.pid_gw_pts;
1721
1722      let xp_val = (el, week) => _.get(xp_dict, week + "_" + el) || 0;
1723      let rp_val = (el, week) => _.get(rp_dict, el + "." + week) || 0;
1724      let xp_sum = (el, weeks) =>
1725        _.sum(weeks.map((i) => xp_val(el, i.gw) * i.mult));
1726      let rp_sum = (el, weeks) =>
1727        _.sum(weeks.map((i) => rp_val(el, i.gw) * i.mult));
1728      let find_best_xp = (el_type, cost, weeks, exclude, target_week) => {
1729        if (cost == undefined) {
1730          return undefined;
1731        }
1732        if (weeks.length == 0) {
1733          return undefined;
1734        }
1735        let candidates = el_data
1736          .filter((i) => i.element_type == el_type)
1737          .filter((i) => gw_ref[target_week + "_" + i.id])
1738          .filter(
1739            (i) =>
1740              parseFloat(gw_ref[target_week + "_" + i.id].price) <=
1741              parseFloat(cost)
1742          )
1743          .filter((i) => !exclude.includes(i.id));
1744        let totals = candidates.map((i) => {
1745          return { ...i, xp_total: xp_sum(i.id, weeks) };
1746        });
1747        let best = _.cloneDeep(_.maxBy(totals, "xp_total"));
1748        best.rp_total = rp_sum(best.id, weeks);
1749        best.cost = gw_ref[weeks[0].gw + "_" + best.id].price;
1750        return best;
1751      };
1752      let find_best_rp = (el_type, cost, weeks, exclude, target_week) => {
1753        if (cost == undefined) {
1754          return undefined;
1755        }
1756        if (weeks.length == 0) {
1757          return undefined;
1758        }
1759        let candidates = el_data
1760          .filter((i) => i.element_type == el_type)
1761          .filter((i) => gw_ref[target_week + "_" + i.id])
1762          .filter(
1763            (i) =>
1764              parseFloat(gw_ref[target_week + "_" + i.id].price) <=
1765              parseFloat(cost)
1766          )
1767          .filter((i) => !exclude.includes(i.id));
1768        let totals = candidates.map((i) => {
1769          return { ...i, rp_total: rp_sum(i.id, weeks) };
1770        });
1771        let best = _.cloneDeep(_.maxBy(totals, "rp_total"));
1772        best.xp_total = xp_sum(best.id, weeks);
1773        best.cost = gw_ref[weeks[0].gw + "_" + best.id].price;
1774        return best;
1775      };
1776
1777      all_gws.forEach((w) => {
1778        if (team_data[w] == undefined) {
1779          return;
1780        }
1781        let tr_count = team_data[w].entry_history.event_transfers;
1782        let tr_cost = team_data[w].entry_history.event_transfers_cost;
1783        let chip = team_data[w].active_chip;
1784        if (
1785          w == 1 ||
1786          tr_count == 0 ||
1787          chip == "freehit" ||
1788          chip == "wildcard"
1789        ) {
1790          return;
1791        }
1792        let this_gw_team = team_data[w].picks.map((i) => i.element);
1793        let last_gw_team = undefined;
1794        if (team_data[w - 1].active_chip == "freehit") {
1795          last_gw_team = team_data[w - 2].picks.map((i) => i.element);
1796        } else {
1797          last_gw_team = team_data[w - 1].picks.map((i) => i.element);
1798        }
1799        let sold = _.cloneDeep(
1800          _.difference(last_gw_team, this_gw_team).map((i) => elements[i])
1801        );
1802        let bought = _.cloneDeep(
1803          _.difference(this_gw_team, last_gw_team).map((i) => elements[i])
1804        );
1805        let gw_transfer = [];
1806        sold.forEach((p) => {
1807          if (p.element_type == 5) {
1808            return;
1809          }
1810          let match = bought.find(
1811            (i) => i.used == undefined && i.element_type == p.element_type
1812          );
1813          match.used = true;
1814          let new_p = match.id;
1815          // check future GWs for new player
1816          future_five = _.range(w, w + 5);
1817          let future_plays = future_five
1818            .filter((j) => team_data[j])
1819            .map((j) => {
1820              return {
1821                gw: j,
1822                play: team_data[j].picks.find((k) => k.element == new_p),
1823              };
1824            })
1825            .filter((j) => j.play != undefined)
1826            .filter((j) => j.play.multiplier > 0);
1827          future_plays.forEach((f) => {
1828            f.mult = f.play.multiplier;
1829          });
1830          let bought_player_plays = future_plays.map((i) => {
1831            return {
1832              ...i,
1833              xp: xp_dict[i.gw + "_" + new_p] || 0,
1834              rp: rp_val(new_p, i.gw),
1835            };
1836          });
1837          let sold_player_plays = future_plays
1838            .map((i) => {
1839              return { gw: i.gw, mult: i.play.multiplier };
1840            })
1841            .map((i) => {
1842              return {
1843                ...i,
1844                xp: xp_dict[i.gw + "_" + p.id] || 0,
1845                rp: rp_val(p.id, i.gw),
1846              };
1847            });
1848
1849          let sold_xp_total = _.sum(
1850            Object.values(sold_player_plays).map((i) => i.xp * i.mult)
1851          );
1852          let bought_xp_total = _.sum(
1853            Object.values(bought_player_plays).map((i) => i.xp * i.mult)
1854          );
1855          let xp_diff = bought_xp_total - sold_xp_total;
1856
1857          let sold_rp_total = _.sum(
1858            Object.values(sold_player_plays).map((i) => i.rp * i.mult)
1859          );
1860          let bought_rp_total = _.sum(
1861            Object.values(bought_player_plays).map((i) => i.rp * i.mult)
1862          );
1863          let rp_diff = bought_rp_total - sold_rp_total;
1864
1865          let gw_list = future_plays
1866            .map((i) =>
1867              i.play.multiplier > 1
1868                ? `${i.gw} (${i.play.multiplier == 2 ? "C" : "TC"})`
1869                : i.gw
1870            )
1871            .join(", ");
1872
1873          let fs_optimal = false;
1874          let hs_optimal = false;
1875          let missed_rp = 0;
1876          let missed_xp = 0;
1877
1878          let xp_ratio_raw = undefined;
1879          let xp_ratio = undefined;
1880
1881          let best_xp = find_best_xp(
1882            elements[p.id].element_type,
1883            gw_ref?.[w + "_" + match.id]?.price,
1884            future_plays,
1885            last_gw_team,
1886            w
1887          );
1888          if (best_xp != undefined) {
1889            best_xp.xp_diff = best_xp.xp_total - sold_xp_total;
1890            best_xp.rp_diff = best_xp.rp_total - sold_rp_total;
1891            fs_optimal = best_xp.id == match.id;
1892            missed_xp = best_xp.xp_total - bought_xp_total;
1893            xp_ratio_raw = (bought_xp_total / best_xp.xp_total) * 100;
1894            if (best_xp.xp_total == 0) {
1895              xp_ratio_raw = 100;
1896            }
1897            xp_ratio = rounded(xp_ratio_raw, 1) + "%";
1898          }
1899          let best_rp = find_best_rp(
1900            elements[p.id].element_type,
1901            gw_ref[w + "_" + match.id]?.price,
1902            future_plays,
1903            last_gw_team,
1904            w
1905          );
1906          if (best_rp != undefined) {
1907            best_rp.xp_diff = best_rp.xp_total - sold_xp_total;
1908            best_rp.rp_diff = best_rp.rp_total - sold_rp_total;
1909            hs_optimal = best_rp.id == match.id;
1910            missed_rp = best_rp.rp_total - bought_rp_total;
1911          }
1912          gw_transfer.push({
1913            gw: w,
1914            sold: p.id,
1915            bought: match.id,
1916            gw_list,
1917            future_plays,
1918            bought_player_plays,
1919            sold_player_plays,
1920            xp_diff,
1921            rp_diff,
1922            sold_price: gw_ref[w + "_" + p.id]?.price ?? 0,
1923            bought_price: gw_ref[w + "_" + match.id]?.price ?? 0,
1924            best_xp,
1925            best_rp,
1926            hs_optimal,
1927            fs_optimal,
1928            best_xp_tr: false,
1929            best_rp_tr: false,
1930            missed_xp,
1931            missed_rp,
1932            xp_ratio_raw,
1933            xp_ratio,
1934          });
1935        });
1936
1937        analysis.gws[w] = gw_transfer;
1938      });
1939
1940      let all_tr = Object.values(analysis.gws)
1941        .flat()
1942        .filter((i) => i.best_xp != undefined);
1943      analysis.fs_ratio =
1944        rounded(
1945          (all_tr.filter((i) => i.fs_optimal).length / all_tr.length) * 100,
1946          1
1947        ) + "%";
1948      analysis.hs_ratio =
1949        rounded(
1950          (all_tr.filter((i) => i.hs_optimal).length / all_tr.length) * 100,
1951          1
1952        ) + "%";
1953
1954      best_xp_transfer = _.maxBy(all_tr, "xp_diff");
1955      if (best_xp_transfer) {
1956        best_xp_transfer.best_xp_tr = true;
1957      }
1958      best_rp_transfer = _.maxBy(all_tr, "rp_diff");
1959      if (best_rp_transfer) {
1960        best_rp_transfer.best_rp_tr = true;
1961      }
1962      analysis.tr_optimality =
1963        _.sum(all_tr.map((i) => i.xp_ratio_raw)) / Math.max(all_tr.length, 1);
1964
1965      return analysis;
1966    },
1967  },
1968  methods: {
1969    fetch_team_picks() {
1970      this.loading = true;
1971      this.ready = false;
1972      $("#top_players_table").DataTable().destroy();
1973      this.top_players_table = undefined;
1974      $("#gw_points_table").DataTable().destroy();
1975      this.gw_table = undefined;
1976      $("#all_picks_per_stat").DataTable().destroy();
1977      this.all_picks_table = undefined;
1978      $("#gain_table").DataTable().destroy();
1979      $("#loss_table").DataTable().destroy();
1980      $("#total_gain_loss_table").DataTable().destroy();
1981      $("#total_exp_gain_loss_table").DataTable().destroy();
1982      $("#total_player_luck_table").DataTable().destroy();
1983      $("#all_fdr_table").DataTable().destroy();
1984
1985      this.team_data = {};
1986      let cache = {};
1987      let gw = 1;
1988      let calls = [];
1989
1990      let init_call = get_team_info(this.team_id)
1991        .then((response) => {
1992          this.team_info = response;
1993          let cached_info = JSON.parse(localStorage.getItem("team_info"));
1994          if (cached_info == null) {
1995            cached_info = {};
1996          }
1997          _.set(cached_info, `${app.season}.T${app.team_id}`, response);
1998          if (!this.off_season) {
1999            localStorage.setItem("team_info", JSON.stringify(cached_info));
2000          }
2001        })
2002        .catch((e) => {
2003          // check if localstorage has it
2004          let cached_info = JSON.parse(localStorage.getItem("team_info"));
2005          let team_cached_info = _.get(
2006            cached_info,
2007            app.season + ".T" + app.team_id
2008          );
2009          if (team_cached_info != null) {
2010            this.team_info = team_cached_info;
2011          }
2012        });
2013      calls.push(init_call);
2014
2015      let stored_team_picks = JSON.parse(localStorage.getItem("team_picks"));
2016      let this_season = this.season;
2017      if (stored_team_picks == null) {
2018        stored_team_picks = {};
2019        _.set(stored_team_picks, this_season + ".T" + app.team_id, {});
2020      } else if (
2021        _.get(stored_team_picks, this_season + ".T" + app.team_id) == undefined
2022      ) {
2023        _.set(stored_team_picks, this_season + ".T" + app.team_id, {});
2024      }
2025      let season_picks = stored_team_picks[this_season]["T" + app.team_id];
2026
2027      for (gw = 1; gw <= this.max_gw; gw++) {
2028        console.log("Fetching GW", gw);
2029        let current_gw = gw;
2030        if (
2031          app.caches_enabled &&
2032          current_gw != app.next_gw &&
2033          season_picks != undefined &&
2034          current_gw in season_picks
2035        ) {
2036          app.$set(app.team_data, current_gw, season_picks[current_gw]);
2037        } else {
2038          let call = get_team_picks({
2039            gw: current_gw,
2040            team_id: app.team_id,
2041            force_last_gw: false,
2042          })
2043            .then((response) => {
2044              season_picks[current_gw] = response.body;
2045              app.$set(app.team_data, current_gw, response.body);
2046            })
2047            .catch((e) => {
2048              console.error(e);
2049              let empty_gw = {
2050                active_chip: null,
2051                automatic_subs: [],
2052                entry_history: {
2053                  bank: 0,
2054                  event: current_gw,
2055                  event_transfers: 0,
2056                  event_transfers_cost: 0,
2057                  overall_rank: 0,
2058                  points: 0,
2059                  points_on_bench: 0,
2060                  rank: 0,
2061                  rank_sort: 0,
2062                  total_points: 0,
2063                  value: 0,
2064                },
2065                picks: [],
2066              };
2067              // app.$set(app.team_data, current_gw, empty_gw)
2068              // ignore
2069            });
2070          calls.push(call);
2071        }
2072      }
2073      Promise.allSettled(calls).then(() => {
2074        this.team_data = Object.freeze(this.team_data);
2075        if (!app.off_season) {
2076          localStorage.setItem("team_picks", JSON.stringify(stored_team_picks));
2077        }
2078        setTimeout(() => {
2079          app.$nextTick(() => {
2080            app.ready = true;
2081            app.loading = false;
2082            app.$nextTick(() => {
2083              app.refresh_table();
2084              app.draw_top_5();
2085              app.draw_pos_heatmap();
2086              app.draw_radar_svg();
2087              draw_event_heatmap();
2088              draw_gain_loss_candlestick();
2089              draw_predicted_realized_diff();
2090              draw_risk_reward_plot();
2091              draw_tree_map();
2092              draw_tree_map_loss();
2093              draw_team_season_visual();
2094              draw_point_origin_graph();
2095            });
2096          });
2097        }, 100);
2098      });
2099    },
2100    draw_top_5() {
2101      let players = this.user_player_sum.slice(0, 3).reverse();
2102      for (let i of players) {
2103        draw_player_bar_chart("#player_charts", i.id);
2104      }
2105    },
2106    add_player_plot() {
2107      let pid = document.getElementById("new_player_id").value;
2108      draw_player_bar_chart("#player_charts", pid);
2109    },
2110    draw_pos_heatmap() {
2111      draw_type_heatmap();
2112    },
2113    sample_label(key) {
2114      // numeric sample tiers (e.g. "100", "1000") are overall-rank cutoffs
2115      if (key != "" && !isNaN(Number(key))) {
2116        return "Top " + formatnumber(Number(key));
2117      }
2118      return key;
2119    },
2120    draw_radar_svg() {
2121      draw_radar_map();
2122    },
2123    refresh_table() {
2124      this.top_players_table = $("#top_players_table").DataTable({
2125        lengthChange: false,
2126        order: [],
2127        pageLength: 20,
2128        info: false,
2129        scrollX: true,
2130        fixedHeader: true,
2131        // fixedColumns: true,
2132        buttons: ["copy", "csv"],
2133      });
2134      this.top_players_table.buttons().container().appendTo("#csv_buttons");
2135
2136      this.gw_table = $("#gw_points_table").DataTable({
2137        lengthChange: false,
2138        order: [],
2139        pageLength: 38,
2140        info: false,
2141        paging: false,
2142        searching: false,
2143        // fixedHeader: true,
2144        scrollY: "450px",
2145        // fixedColumns: true,
2146        // buttons: [
2147        //     'copy', 'csv'
2148        // ]
2149      });
2150
2151      this.all_picks_table = $("#all_picks_per_stat").DataTable({
2152        lengthChange: false,
2153        order: [],
2154        info: true,
2155        paging: true,
2156        pageLength: 15,
2157        searching: true,
2158        // scrollY: '450px',
2159        // sScrollX: "100%",
2160        buttons: ["copy", "csv"],
2161        initComplete: function () {
2162          this.api()
2163            .columns()
2164            .every(function () {
2165              var column = this;
2166              var select = $(
2167                '<select class="w-100"><option value=""></option></select>'
2168              )
2169                .appendTo($(column.footer()).empty())
2170                .on("change", function () {
2171                  var val = $.fn.dataTable.util.escapeRegex($(this).val());
2172
2173                  column.search(val ? "^" + val + "$" : "", true, false).draw();
2174                });
2175
2176              column
2177                .data()
2178                .unique()
2179                .sort((a, b) =>
2180                  parseInt(a) ? parseInt(a) - parseInt(b) : a > b ? 1 : -1
2181                )
2182                .each(function (d, j) {
2183                  select.append('<option value="' + d + '">' + d + "</option>");
2184                });
2185            });
2186        },
2187      });
2188      this.all_picks_table.buttons().container().appendTo("#csv_buttons3");
2189
2190      $("#gain_table").DataTable({
2191        lengthChange: false,
2192        order: [],
2193        info: true,
2194        paging: true,
2195        pageLength: 15,
2196        searching: true,
2197        buttons: ["copy", "csv"],
2198        initComplete: function () {
2199          this.api()
2200            .columns()
2201            .every(function () {
2202              var column = this;
2203              var select = $(
2204                '<select class="w-100"><option value=""></option></select>'
2205              )
2206                .appendTo($(column.footer()).empty())
2207                .on("change", function () {
2208                  var val = $.fn.dataTable.util.escapeRegex($(this).val());
2209
2210                  column.search(val ? "^" + val + "$" : "", true, false).draw();
2211                });
2212
2213              column
2214                .data()
2215                .unique()
2216                .sort((a, b) =>
2217                  parseInt(a) ? parseInt(a) - parseInt(b) : a > b ? 1 : -1
2218                )
2219                .each(function (d, j) {
2220                  select.append('<option value="' + d + '">' + d + "</option>");
2221                });
2222            });
2223        },
2224      });
2225
2226      $("#loss_table").DataTable({
2227        lengthChange: false,
2228        order: [],
2229        info: true,
2230        paging: true,
2231        pageLength: 15,
2232        searching: true,
2233        buttons: ["copy", "csv"],
2234        initComplete: function () {
2235          this.api()
2236            .columns()
2237            .every(function () {
2238              var column = this;
2239              var select = $(
2240                '<select class="w-100"><option value=""></option></select>'
2241              )
2242                .appendTo($(column.footer()).empty())
2243                .on("change", function () {
2244                  var val = $.fn.dataTable.util.escapeRegex($(this).val());
2245
2246                  column.search(val ? "^" + val + "$" : "", true, false).draw();
2247                });
2248
2249              column
2250                .data()
2251                .unique()
2252                .sort((a, b) =>
2253                  parseInt(a) ? parseInt(a) - parseInt(b) : a > b ? 1 : -1
2254                )
2255                .each(function (d, j) {
2256                  select.append('<option value="' + d + '">' + d + "</option>");
2257                });
2258            });
2259        },
2260      });
2261
2262      let tg_table = $("#total_gain_loss_table").DataTable({
2263        lengthChange: false,
2264        order: [],
2265        info: true,
2266        paging: true,
2267        pageLength: 15,
2268        searching: true,
2269        scrollX: true,
2270        // sScrollX: "100%",
2271        // responsive: true,
2272        buttons: ["copy", "csv"],
2273        initComplete: function () {
2274          this.api()
2275            .columns()
2276            .every(function () {
2277              var column = this;
2278              var select = $(
2279                '<select class="w-100"><option value=""></option></select>'
2280              )
2281                .appendTo($(column.footer()).empty())
2282                .on("change", function () {
2283                  var val = $.fn.dataTable.util.escapeRegex($(this).val());
2284
2285                  column.search(val ? "^" + val + "$" : "", true, false).draw();
2286                });
2287
2288              column
2289                .data()
2290                .unique()
2291                .sort((a, b) =>
2292                  parseInt(a) ? parseInt(a) - parseInt(b) : a > b ? 1 : -1
2293                )
2294                .each(function (d, j) {
2295                  select.append('<option value="' + d + '">' + d + "</option>");
2296                });
2297            });
2298        },
2299      });
2300
2301      tg_table.buttons().container().appendTo("#csv_buttons4");
2302
2303      let tge_table = $("#total_exp_gain_loss_table").DataTable({
2304        lengthChange: false,
2305        order: [],
2306        info: true,
2307        paging: true,
2308        pageLength: 15,
2309        searching: true,
2310        scrollX: true,
2311        // sScrollX: "100%",
2312        // responsive: true,
2313        buttons: ["copy", "csv"],
2314        initComplete: function () {
2315          this.api()
2316            .columns()
2317            .every(function () {
2318              var column = this;
2319              var select = $(
2320                '<select class="w-100"><option value=""></option></select>'
2321              )
2322                .appendTo($(column.footer()).empty())
2323                .on("change", function () {
2324                  var val = $.fn.dataTable.util.escapeRegex($(this).val());
2325
2326                  column.search(val ? "^" + val + "$" : "", true, false).draw();
2327                });
2328
2329              column
2330                .data()
2331                .unique()
2332                .sort((a, b) =>
2333                  parseInt(a) ? parseInt(a) - parseInt(b) : a > b ? 1 : -1
2334                )
2335                .each(function (d, j) {
2336                  select.append('<option value="' + d + '">' + d + "</option>");
2337                });
2338            });
2339        },
2340      });
2341
2342      tge_table.buttons().container().appendTo("#csv_buttons_exp");
2343
2344      let tgl_table = $("#total_player_luck_table").DataTable({
2345        lengthChange: false,
2346        order: [],
2347        info: true,
2348        paging: true,
2349        pageLength: 15,
2350        searching: true,
2351        scrollX: true,
2352        // sScrollX: "100%",
2353        // responsive: true,
2354        buttons: ["copy", "csv"],
2355        initComplete: function () {
2356          this.api()
2357            .columns()
2358            .every(function () {
2359              var column = this;
2360              var select = $(
2361                '<select class="w-100"><option value=""></option></select>'
2362              )
2363                .appendTo($(column.footer()).empty())
2364                .on("change", function () {
2365                  var val = $.fn.dataTable.util.escapeRegex($(this).val());
2366
2367                  column.search(val ? "^" + val + "$" : "", true, false).draw();
2368                });
2369
2370              column
2371                .data()
2372                .unique()
2373                .sort((a, b) =>
2374                  parseInt(a) ? parseInt(a) - parseInt(b) : a > b ? 1 : -1
2375                )
2376                .each(function (d, j) {
2377                  select.append('<option value="' + d + '">' + d + "</option>");
2378                });
2379            });
2380        },
2381      });
2382
2383      tgl_table.buttons().container().appendTo("#csv_buttons_luck_table");
2384
2385      let fdr_table = $("#all_fdr_table").DataTable({
2386        lengthChange: false,
2387        order: [],
2388        info: true,
2389        paging: true,
2390        pageLength: 15,
2391        searching: true,
2392        scrollX: true,
2393        // sScrollX: "100%",
2394        // responsive: true,
2395        buttons: ["copy", "csv"],
2396        initComplete: function () {
2397          this.api()
2398            .columns()
2399            .every(function () {
2400              var column = this;
2401              var select = $(
2402                '<select class="w-100"><option value=""></option></select>'
2403              )
2404                .appendTo($(column.footer()).empty())
2405                .on("change", function () {
2406                  var val = $.fn.dataTable.util.escapeRegex($(this).val());
2407
2408                  column.search(val ? "^" + val + "$" : "", true, false).draw();
2409                });
2410
2411              column
2412                .data()
2413                .unique()
2414                .sort((a, b) =>
2415                  parseInt(a) ? parseInt(a) - parseInt(b) : a > b ? 1 : -1
2416                )
2417                .each(function (d, j) {
2418                  select.append('<option value="' + d + '">' + d + "</option>");
2419                });
2420            });
2421        },
2422      });
2423      fdr_table.buttons().container().appendTo("#csv_buttons5");
2424    },
2425    add_ID_to_compare() {
2426      let val = document.getElementById("new_id").value;
2427      document.getElementById("new_id").value = "";
2428
2429      let gw = 1;
2430      let calls = [];
2431      responses = {};
2432      let opp_info = {};
2433
2434      let init_call = get_team_info(val).then((response) => {
2435        opp_info = response;
2436      });
2437      calls.push(init_call);
2438
2439      for (gw = 1; gw <= this.max_gw; gw++) {
2440        console.log("Fetching GW", gw);
2441        let current_gw = gw;
2442        let call = get_team_picks({
2443          gw: current_gw,
2444          team_id: val,
2445          force_last_gw: false,
2446        }).then((response) => {
2447          responses[current_gw] = response.body;
2448        });
2449        calls.push(call);
2450      }
2451      Promise.allSettled(calls).then(() => {
2452        this.rival_info.push({
2453          info: opp_info,
2454          picks: responses,
2455        });
2456        draw_radar_map();
2457      });
2458    },
2459    clear_rivals() {
2460      this.rival_info = [];
2461      draw_radar_map();
2462    },
2463    invalidate_cache() {
2464      this.$nextTick(() => {
2465        var table = $("#gain_table").DataTable();
2466        table.cells("td").invalidate().draw();
2467        table = $("#loss_table").DataTable();
2468        table.cells("td").invalidate().draw();
2469        table = $("#total_gain_loss_table").DataTable();
2470        table = $("#total_exp_gain_loss_table").DataTable();
2471        table = $("#total_player_luck_table").DataTable();
2472        table.cells("td").invalidate().draw();
2473      });
2474    },
2475    breakpoint() {
2476      debugger;
2477    },
2478  },
2479});
2480
2481function get_team_stats_picks(picks) {
2482  let team_data = picks;
2483  let fpl_data = app.fpl_element;
2484  let stat_detailed = app.stats_per_gw_detailed;
2485  let all_team_picks = Object.keys(team_data)
2486    .map((i) => team_data[i].picks.map((j) => ({ gw: i, ...j })))
2487    .flat();
2488  let pick_mult_map = all_team_picks.map((i) => [
2489    i.gw + "," + i.element,
2490    i.multiplier,
2491  ]);
2492  let pick_mult = Object.fromEntries(pick_mult_map);
2493  let picked_stats = stat_detailed.filter(
2494    (i) => i.gw + "," + i.id in pick_mult
2495  );
2496  picked_stats.forEach((p) => {
2497    p.multiplier = pick_mult[p.gw + "," + p.id];
2498    p.total_points = p.points * p.multiplier;
2499    p.eltype = fpl_data[p.id].element_type;
2500    p.name = fpl_data[p.id].web_name;
2501  });
2502
2503  let clean_sheets = picked_stats.filter(
2504    (i) => i.eltype <= 2 && i.identifier == "clean_sheets" && i.multiplier > 0
2505  );
2506  let defense_picks = _.cloneDeep(
2507    picked_stats.filter(
2508      (i) => i.eltype <= 2 && i.identifier == "minutes" && i.multiplier > 0
2509    )
2510  );
2511  for (let p of defense_picks) {
2512    let match = clean_sheets.filter(
2513      (i) => i.id == p.id && i.gw == p.gw && i.fixture == p.fixture
2514    );
2515    if (match.length != 0) {
2516      p.success = true;
2517      p.returns = match[0].total_points;
2518    } else {
2519      p.success = false;
2520      p.returns = 0;
2521    }
2522  }
2523  let cs_stat = {
2524    count: clean_sheets.length,
2525    total: defense_picks.length,
2526    values: defense_picks,
2527    info: "Rate of CS gains out of all GK and DF",
2528  };
2529
2530  let goal_count = picked_stats.filter(
2531    (i) => i.eltype >= 3 && i.identifier == "goals_scored" && i.multiplier > 0
2532  );
2533  let attack_picks = _.cloneDeep(
2534    picked_stats.filter(
2535      (i) => i.eltype >= 3 && i.identifier == "minutes" && i.multiplier > 0
2536    )
2537  );
2538  for (let p of attack_picks) {
2539    let match = goal_count.filter(
2540      (i) => i.id == p.id && i.gw == p.gw && i.fixture == p.fixture
2541    );
2542    if (match.length != 0) {
2543      p.success = true;
2544      p.returns = match[0].total_points;
2545    } else {
2546      p.success = false;
2547      p.returns = 0;
2548    }
2549  }
2550  let goal_stat = {
2551    count: goal_count.length,
2552    total: attack_picks.length,
2553    values: attack_picks,
2554    info: "Rate of goal returns out of all MD and FW",
2555  };
2556
2557  let assists_count = picked_stats.filter(
2558    (i) => i.eltype >= 3 && i.identifier == "assists" && i.multiplier > 0
2559  );
2560  let assist_attack_picks = _.cloneDeep(
2561    picked_stats.filter(
2562      (i) => i.eltype >= 3 && i.identifier == "minutes" && i.multiplier > 0
2563    )
2564  );
2565  for (let p of assist_attack_picks) {
2566    let match = assists_count.filter(
2567      (i) => i.id == p.id && i.gw == p.gw && i.fixture == p.fixture
2568    );
2569    if (match.length != 0) {
2570      p.success = true;
2571      p.returns = match[0].total_points;
2572    } else {
2573      p.success = false;
2574      p.returns = 0;
2575    }
2576  }
2577  let assist_stat = {
2578    count: assists_count.length,
2579    total: assist_attack_picks.length,
2580    values: assist_attack_picks,
2581    info: "Rate of assist returns out of all MD and FW",
2582  };
2583
2584  let defcon_count = picked_stats.filter(
2585    (i) =>
2586      i.eltype >= 2 &&
2587      i.identifier == "defensive_contribution" &&
2588      i.multiplier > 0
2589  );
2590  let defcon_picks = _.cloneDeep(
2591    picked_stats.filter(
2592      (i) => i.eltype >= 2 && i.identifier == "minutes" && i.multiplier > 0
2593    )
2594  );
2595  for (let p of defcon_picks) {
2596    let match = defcon_count.filter(
2597      (i) => i.id == p.id && i.gw == p.gw && i.fixture == p.fixture
2598    );
2599    if (match.length != 0) {
2600      p.success = true;
2601      p.returns = match[0].total_points;
2602    } else {
2603      p.success = false;
2604      p.returns = 0;
2605    }
2606  }
2607  let defcon_stat = {
2608    count: defcon_count.length,
2609    total: defcon_picks.length,
2610    values: defcon_picks,
2611    info: "Rate of defensive contribution returns out of all outfield players",
2612  };
2613
2614  let bonus_count = picked_stats.filter(
2615    (i) => i.identifier == "bonus" && i.multiplier > 0
2616  );
2617  let all_count = _.cloneDeep(
2618    picked_stats.filter((i) => i.identifier == "minutes" && i.multiplier > 0)
2619  );
2620  for (let p of all_count) {
2621    let match = bonus_count.filter(
2622      (i) => i.id == p.id && i.gw == p.gw && i.fixture == p.fixture
2623    );
2624    if (match.length != 0) {
2625      p.success = true;
2626      p.returns = match[0].total_points;
2627    } else {
2628      p.success = false;
2629      p.returns = 0;
2630    }
2631  }
2632  let bonus_stat = {
2633    count: bonus_count.length,
2634    total: all_count.length,
2635    values: all_count,
2636    info: "Rate of bonus returns out of all players",
2637  };
2638
2639  let captain_pts = picked_stats.filter((i) => i.multiplier > 1);
2640  let captain_game_pts = {};
2641  captain_pts.forEach((c) => {
2642    if (!(c.fixture in captain_game_pts)) {
2643      captain_game_pts[c.fixture] = 0;
2644    }
2645    captain_game_pts[c.fixture] += c.total_points;
2646  });
2647  let non_blank_captain = Object.values(captain_game_pts).filter((i) => i > 7);
2648  let captain_count = _.cloneDeep(
2649    picked_stats.filter((i) => i.multiplier > 1 && i.identifier == "minutes")
2650  );
2651  for (let p of captain_count) {
2652    let match = captain_game_pts[p.fixture] || 0;
2653    if (match >= 7) {
2654      p.success = true;
2655    }
2656    p.returns = match;
2657  }
2658  let captain_stat = {
2659    count: non_blank_captain.length,
2660    total: captain_count.length,
2661    values: captain_count,
2662    info: "Rate of non-blanking (7+ pts) captain returns",
2663  };
2664
2665  return {
2666    "Clean Sheet": cs_stat,
2667    "Def Con": defcon_stat,
2668    Goal: goal_stat,
2669    Assist: assist_stat,
2670    Bonus: bonus_stat,
2671    Captain: captain_stat,
2672  };
2673}
2674
2675function export_raw_data() {
2676  let obj = Object.entries(app.$data).filter((i) => !i[0].includes("_table"));
2677  downloadToFile(
2678    JSON.stringify(obj, undefined, 2),
2679    "myFPLdata.json",
2680    "application/json"
2681  );
2682}
2683
2684function export_all_data() {
2685  let raw_data = Object.entries(app.$data).filter(
2686    (i) => !i[0].includes("_table")
2687  );
2688  let comp_data = Object.fromEntries(
2689    Object.keys(app.$options.computed).map((i) => [i, app[i]])
2690  );
2691  let obj = { ...raw_data, ...comp_data };
2692  downloadToFile(
2693    JSON.stringify(obj, undefined, 2),
2694    "myFPLdata_extended.json",
2695    "application/json"
2696  );
2697}
2698
2699function download_excel() {
2700  // Create a new workbook
2701  const wb = XLSX.utils.book_new();
2702
2703  // sheet 0: team info
2704  let tinfo_raw = _.cloneDeep(app.team_info);
2705  delete tinfo_raw["kit"];
2706  let tinfo = Object.entries(tinfo_raw).map((i) => {
2707    return { key: i[0], value: i[1] };
2708  });
2709  // xlsx.utils.json_to_sheet
2710  let s0 = XLSX.utils.json_to_sheet(tinfo);
2711  XLSX.utils.book_append_sheet(wb, s0, "Team Info");
2712
2713  // sheet 1: gw rank pts chip picks
2714  let td = _.cloneDeep(app.team_data);
2715  let ent = Object.entries(td).map((i) => {
2716    return { gw: i[0], ...i[1] };
2717  });
2718  ent.forEach((e) => {
2719    for (key of Object.keys(e.entry_history)) {
2720      e[key] = e.entry_history[key];
2721    }
2722    delete e["entry_history"];
2723    e["pick_ids"] = e["picks"].map((i) => i.element).join(",");
2724    e["pick_names"] = e["picks"]
2725      .map((i) => app.fpl_element[i.element].web_name)
2726      .join(",");
2727    delete e.picks;
2728  });
2729  let s1 = XLSX.utils.json_to_sheet(ent);
2730  XLSX.utils.book_append_sheet(wb, s1, "GW Info");
2731
2732  // sheet 2: team of the year
2733  let toty = _.cloneDeep(app.toty_final);
2734  for (let t of toty) {
2735    for (let k of Object.keys(t.player)) {
2736      t[k] = t.player[k];
2737    }
2738    delete t.player;
2739  }
2740  let s2 = XLSX.utils.json_to_sheet(toty);
2741  XLSX.utils.book_append_sheet(wb, s2, "Team of the Year");
2742
2743  // sheet 3: player picks
2744  let td3 = _.cloneDeep(app.team_data);
2745  let ent3 = Object.entries(td3).map((i) => {
2746    return { gw: i[0], ...i[1] };
2747  });
2748  let p_picks = [];
2749  for (let entry of ent3) {
2750    for (let pick of entry.picks) {
2751      pick["name"] = app.fpl_element[pick["element"]].web_name;
2752      p_picks.push(pick);
2753    }
2754  }
2755  let s3 = XLSX.utils.json_to_sheet(p_picks);
2756  XLSX.utils.book_append_sheet(wb, s3, "GW Picks");
2757
2758  // sheet 4: total realized gain/loss
2759  let d4 = _.cloneDeep(app.user_ownership_gain_loss.combined_per_player);
2760  d4 = d4.map((i) => {
2761    return { name: app.fpl_element[i.id].web_name, ...i };
2762  });
2763  let s4 = XLSX.utils.json_to_sheet(d4);
2764  XLSX.utils.book_append_sheet(wb, s4, "Player Gain - Loss");
2765
2766  // sheet 5: total predicted gain/loss
2767  // already included
2768  // sheet 6: total luck
2769  // skip
2770  // sheet 7: team picks over season? + info cols
2771  // skip
2772
2773  // sheet 8: points by acquisition type (in/wc/tr/fh)
2774  let acq_values = [];
2775  let gw_list = _.uniq(p_picks.map((i) => i.gw));
2776  let origin_types = _.uniq(p_picks.map((i) => i.origin));
2777  for (let gw of gw_list) {
2778    let gw_picks = p_picks.filter((i) => i.gw == gw);
2779    for (let origin of origin_types) {
2780      let gw_origin = gw_picks.filter((i) => i.origin == origin);
2781      acq_values.push({
2782        gw: gw,
2783        origin: origin,
2784        pts: _.sum(gw_origin.map((i) => i.eff_points)),
2785        lineup: gw_origin.filter((i) => i.multiplier > 0).length,
2786        squad: gw_origin.length,
2787      });
2788    }
2789  }
2790  let s8 = XLSX.utils.json_to_sheet(acq_values);
2791  XLSX.utils.book_append_sheet(wb, s8, "Point Origins by GW");
2792
2793  // sheet 9: total counts key events per GW
2794  let cnt_data = _.cloneDeep(app.user_stats_per_gw.points);
2795  for (let c of cnt_data) {
2796    let keys = Object.keys(c.info.type_names);
2797    for (let key of keys) {
2798      c[key + "_count"] = c.info.type_sums[key];
2799    }
2800    for (let key of keys) {
2801      c[key + "_text"] = c.info.type_names[key];
2802    }
2803    delete c.info;
2804  }
2805  let s9 = XLSX.utils.json_to_sheet(cnt_data);
2806  XLSX.utils.book_append_sheet(wb, s9, "Event Counts per GW");
2807
2808  // sheet 10: point distribution per event type
2809  let event_pts = _.cloneDeep(app.user_stats_per_gw?.type_grouped);
2810  let ep_list = _.map(event_pts, (value, key) => {
2811    return { type: key, values: value };
2812  });
2813  let ep_final = [];
2814  for (let ep of ep_list) {
2815    let user_stats_per_gw = app.user_stats_per_gw;
2816
2817    let key = ep.type;
2818    let value = ep.values;
2819
2820    let count = player_stat_types[ep.type]?.pp ? ep.values.length : "-";
2821    let total = getSum(value.map((i) => parseInt(i.value)));
2822    let per_player = player_stat_types[key].pp
2823      ? rounded(
2824          getSum(value.map((i) => parseInt(i.value))) /
2825            user_stats_per_gw.total_picks,
2826          2
2827        )
2828      : "-";
2829    let points = getSum(value.map((i) => parseInt(i.points * i.multiplier)));
2830    let percentage = rounded(
2831      (getSum(value.map((i) => parseInt(i.points * i.multiplier))) /
2832        user_stats_per_gw.overall_total) *
2833        100,
2834      1
2835    );
2836
2837    ep_final.push({
2838      type: ep.type,
2839      count: count,
2840      total: total,
2841      per_player: per_player,
2842      points: points,
2843      percentage: percentage,
2844    });
2845  }
2846  let s10 = XLSX.utils.json_to_sheet(ep_final);
2847  XLSX.utils.book_append_sheet(wb, s10, "Event Points Total");
2848
2849  // sheet 11: detailed points returns per event/gw
2850
2851  let d11 = _(detailed_point_cache)
2852    .groupBy(1)
2853    .map((value, key) => {
2854      return {
2855        type: key,
2856        ..._.fromPairs(
2857          value.map((j) => [j[0] == "Total" ? j[0] : "GW" + j[0], j[2]])
2858        ),
2859      };
2860    })
2861    .value();
2862  let s11 = XLSX.utils.json_to_sheet(d11);
2863  XLSX.utils.book_append_sheet(wb, s11, "Event Points per GW");
2864
2865  // sheet 12: target event ratios
2866  let d12 = _.map(_.cloneDeep(app.user_picks_custom_stats), (value, key) => {
2867    return {
2868      target: key,
2869      count: value.count,
2870      picks: value.total,
2871      success_ratio: _.round((100 * value.count) / value.total, 2),
2872      info: value.info,
2873    };
2874  });
2875  let s12 = XLSX.utils.json_to_sheet(d12);
2876  XLSX.utils.book_append_sheet(wb, s12, "Target Event Ratios");
2877
2878  // sheet 13: timings of ownership?
2879  // skip: not sure
2880
2881  // sheet 14: gain analysis
2882  let d14 = _.cloneDeep(app.user_ownership_gain_loss.gains).map((i) => {
2883    return { name: app.fpl_element[i.id].web_name, ...i };
2884  });
2885  let s14 = XLSX.utils.json_to_sheet(d14);
2886  XLSX.utils.book_append_sheet(wb, s14, "Gain Analysis");
2887  // sheet 15: loss analysis
2888  let d15 = _.cloneDeep(app.user_ownership_gain_loss.losses).map((i) => {
2889    return { name: app.fpl_element[i.id].web_name, ...i };
2890  });
2891  let s15 = XLSX.utils.json_to_sheet(d15);
2892  XLSX.utils.book_append_sheet(wb, s15, "Loss Analysis");
2893
2894  // sheet 16: predicted realized performance
2895  let d16 = Object.values(app.user_gw_results?.data);
2896  let s16 = XLSX.utils.json_to_sheet(d16);
2897  XLSX.utils.book_append_sheet(wb, s16, "Exp vs Real Performance");
2898
2899  // sheet 17: transfer quality
2900  let d17 = _.cloneDeep(Object.values(app.transfer_analysis.gws).flat());
2901  for (let d of d17) {
2902    d["bought_name"] = app.fpl_element[d.bought].web_name;
2903    d["sold_name"] = app.fpl_element[d.sold].web_name;
2904    d["best_rp"] = d["best_rp"]?.web_name || "-";
2905    d["best_xp"] = d["best_xp"]?.web_name || "-";
2906    delete d["bought_player_plays"];
2907    delete d["future_plays"];
2908    delete d["sold_player_plays"];
2909  }
2910  let s17 = XLSX.utils.json_to_sheet(d17);
2911  XLSX.utils.book_append_sheet(wb, s17, "Transfer Quality");
2912
2913  // sheet 18: formation
2914  let d18 = _.cloneDeep(app.user_formation_analysis_by_gw.sorted);
2915  let s18 = XLSX.utils.json_to_sheet(d18);
2916  XLSX.utils.book_append_sheet(wb, s18, "Formations");
2917
2918  // // Create data for the first sheet
2919  // const ws1Data = [
2920  //     ["Name", "Age", "Email"],
2921  //     ["John Doe", 30, "[email protected]"],
2922  //     ["Jane Smith", 25, "[email protected]"]
2923  // ];
2924  // // Create a worksheet from the data
2925  // const ws1 = XLSX.utils.aoa_to_sheet(ws1Data);
2926  // // Append the worksheet to the workbook
2927  // XLSX.utils.book_append_sheet(wb, ws1, "Sheet1");
2928
2929  // // Create data for the second sheet
2930  // const ws2Data = [
2931  //     ["Product", "Price", "Quantity"],
2932  //     ["Apple", 1.0, 10],
2933  //     ["Banana", 0.5, 20]
2934  // ];
2935  // // Create a worksheet from the data
2936  // const ws2 = XLSX.utils.aoa_to_sheet(ws2Data);
2937  // // Append the worksheet to the workbook
2938  // XLSX.utils.book_append_sheet(wb, ws2, "Sheet2");
2939
2940  // // Write the workbook to a file and trigger a download
2941  // // XLSX.writeFile(wb, "MultiSheetExample.xlsx");
2942  // // XLSX.writeFile(wb, "Presidents.xlsx", { compression: true });
2943
2944  // Write the workbook to a binary string
2945  const wbout = XLSX.write(wb, { bookType: "xlsx", type: "binary" });
2946
2947  // Convert the binary string to a Blob
2948  function s2ab(s) {
2949    const buf = new ArrayBuffer(s.length);
2950    const view = new Uint8Array(buf);
2951    for (let i = 0; i < s.length; i++) {
2952      view[i] = s.charCodeAt(i) & 0xff;
2953    }
2954    return buf;
2955  }
2956  const blob = new Blob([s2ab(wbout)], { type: "application/octet-stream" });
2957
2958  // Create a link element
2959  const url = URL.createObjectURL(blob);
2960  const a = document.createElement("a");
2961  a.href = url;
2962  a.download = "FPL_Optimized_Season_Highlights.xlsx";
2963  document.body.appendChild(a);
2964  a.click();
2965  document.body.removeChild(a);
2966  URL.revokeObjectURL(url);
2967}
2968
2969function draw_player_bar_chart(div_id, id) {
2970  if (!app.is_ready) {
2971    return;
2972  }
2973
2974  const raw_width = 500;
2975  const raw_height = 120;
2976
2977  const margin = { top: 20, right: 20, bottom: 25, left: 20 },
2978    width = raw_width - margin.left - margin.right,
2979    height = raw_height - margin.top - margin.bottom;
2980
2981  let raw_data = app.pid_gw_pts[id] || [];
2982  let pts_data = Object.entries(raw_data); // app.pid_gw_pts[player_id]
2983  let min_y = Math.min(0, Math.min(...pts_data.map((i) => i[1])));
2984  let max_y = Math.max(Math.max(...pts_data.map((i) => i[1])) + 4, 6);
2985  // let max_x = Math.max(Math.max(...pts_data.map(i => i[0])), parseInt(gw))
2986  let max_x = 38;
2987
2988  let team_pick_gws = app.user_player_gws;
2989  let user_gws = team_pick_gws.filter((i) => i.id == id);
2990  let lineup_gws = user_gws
2991    .filter((i) => i.id == id && i.multiplier > 0)
2992    .map((i) => i.gw);
2993  let bench_gws = user_gws
2994    .filter((i) => i.id == id && i.multiplier == 0)
2995    .map((i) => i.gw);
2996  let captain_gws = user_gws
2997    .filter((i) => i.id == id && i.multiplier > 1)
2998    .map((i) => [i.gw, i.multiplier, raw_data[i.gw]]);
2999
3000  let this_id = `player-svg-${id}`;
3001
3002  const svg = d3
3003    .select(div_id)
3004    // .append("svg")
3005    .insert("svg", ":first-child")
3006    .attr(
3007      "viewBox",
3008      `0 0  ${width + margin.left + margin.right} ${
3009        height + margin.top + margin.bottom
3010      }`
3011    )
3012    .attr("class", "pull-center")
3013    .style("display", "block")
3014    .style("margin-bottom", "10px")
3015    .attr("id", `player-svg-${id}`)
3016    .append("g")
3017    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
3018
3019  let x = d3
3020    .scaleBand()
3021    .range([0, width])
3022    .domain(_.range(1, max_x + 1))
3023    .padding(0.2);
3024  svg
3025    .append("g")
3026    .attr("transform", "translate(0," + height + ")")
3027    .call(d3.axisBottom(x).tickSize(0));
3028  let x2 = d3
3029    .scaleBand()
3030    .range([0, width])
3031    .domain(_.range(1, max_x + 1))
3032    .padding(0);
3033
3034  let y = d3.scaleLinear().domain([min_y, max_y]).range([height, 0]);
3035  svg
3036    .append("g")
3037    .call(d3.axisRight(y).tickSize(width).tickValues(d3.range(min_y, max_y, 4)))
3038    .call((g) =>
3039      g
3040        .selectAll(".tick line")
3041        .attr("stroke-opacity", 0.2)
3042        .attr("stroke", "#9a9a9a")
3043    )
3044    .call((g) =>
3045      g
3046        .selectAll(".tick text")
3047        .attr("x", -10)
3048        .attr("font-size", "5pt")
3049        .attr("fill", "#9a9a9a")
3050        .attr("text-anchor", "end")
3051    );
3052
3053  svg.call((g) => g.selectAll(".domain").attr("opacity", 0));
3054
3055  svg.call((s) =>
3056    s.selectAll(".tick").attr("font-size", "5pt").attr("fill", "white")
3057  );
3058
3059  // zero line
3060  svg
3061    .append("g")
3062    .append("line")
3063    .attr("x1", x(0))
3064    .attr("y1", y(0))
3065    .attr("x2", width)
3066    .attr("y2", y(0))
3067    .style("stroke", "white")
3068    .style("stroke-opacity", 0.5)
3069    .style("stroke-width", 1)
3070    .style("pointer-events", "none");
3071
3072  svg
3073    .selectAll()
3074    .data(lineup_gws)
3075    .enter()
3076    .append("rect")
3077    .attr("x", (d) => x2(d))
3078    .attr("y", (d) => y(max_y))
3079    .attr("width", (d) => x2.bandwidth())
3080    .attr("height", (d) =>
3080 height - y(max_y))
3081    .attr("class", "lineup_bar");
3082
3083  svg
3084    .selectAll()
3085    .data(bench_gws)
3086    .enter()
3087    .append("rect")
3088    .attr("x", (d) => x2(d))
3089    .attr("y", (d) => y(max_y))
3090    .attr("width", (d) => x2.bandwidth())
3091    .attr("height", (d) => height - y(max_y))
3092    .attr("class", "bench_bar");
3093
3094  svg
3095    .selectAll()
3096    .data(pts_data)
3097    .enter()
3098    .append("rect")
3099    .attr("x", (d) => x(d[0]))
3100    .attr("y", (d) => (d[1] > 0 ? y(d[1]) : y(0)))
3101    .attr("width", x.bandwidth())
3102    .attr("height", (d) => (d[1] > 0 ? y(0) - y(d[1]) : y(d[1]) - y(0)))
3103    .attr("class", "player_point_bar");
3104
3105  svg
3106    .selectAll()
3107    .data(captain_gws)
3108    .enter()
3109    .append("text")
3110    .attr("text-anchor", "middle")
3111    .attr("alignment-baseline", "bottom")
3112    .attr("dominant-baseline", "bottom")
3113    .attr("x", (d) => x(d[0]) + x.bandwidth() / 2)
3114    .attr("y", (d) => y(d[2] || 0) - 5)
3115    .attr("font-size", "5pt")
3116    .attr("fill", "white")
3117    .text((d) => "x" + d[1]);
3118
3119  // Plot title
3120  svg
3121    .append("text")
3122    .attr("text-anchor", "middle")
3123    .attr("alignment-baseline", "middle")
3124    .attr("dominant-baseline", "middle")
3125    .attr("x", width / 2)
3126    .attr("y", -5)
3127    .attr("font-size", "6pt")
3128    .attr("fill", "white")
3129    .text(`${app.fpl_element[id].web_name}`);
3130
3131  // x-title
3132  svg
3133    .append("text")
3134    .attr("text-anchor", "middle")
3135    .attr("x", width / 2)
3136    .attr("y", height + 20)
3137    .attr("font-size", "5pt")
3138    .attr("fill", "white")
3139    .text("Gameweeks");
3140
3141  // y-title
3142  svg
3143    .append("text")
3144    .attr("text-anchor", "middle")
3145    .attr("x", -5)
3146    .attr("y", -3)
3147    .attr("font-size", "5pt")
3148    .attr("fill", "white")
3149    .text("Points");
3150
3151  // let cb = svg.append("g")
3152
3153  mouseclick = (d) => {
3154    $("#" + this_id).remove();
3155  };
3156
3157  svg
3158    .append("rect")
3159    .attr("x", width - 20)
3160    .attr("y", -20)
3161    .attr("width", 20)
3162    .attr("height", 20)
3163    .attr("fill", "#00000050")
3164    .attr("class", "close_box")
3165    .on("click", mouseclick);
3166
3167  svg
3168    .append("text")
3169    .attr("text-anchor", "middle")
3170    .attr("alignment-baseline", "middle")
3171    .attr("dominant-baseline", "middle")
3172    .attr("x", width - 10)
3173    .attr("y", -10)
3174    .attr("font-size", "6pt")
3175    .attr("fill", "white")
3176    .attr("class", "close_text")
3177    .text("X");
3178}
3179
3180function draw_type_heatmap() {
3181  if (!app.is_ready) {
3182    return;
3183  }
3184
3185  const raw_width = 500;
3186  const raw_height = 400;
3187
3188  const margin = { top: 20, right: 10, bottom: 5, left: 50 },
3189    width = raw_width - margin.left - margin.right,
3190    height = raw_height - margin.top - margin.bottom;
3191
3192  let data = app.user_points_by_eltype;
3193  let xvals = ["GK", "DF", "MD", "FW", "Total"];
3194  let yvals = ["Budget", "Mid-Price", "Premium", "Total"];
3195  let valmax = Math.max(...data.map((i) => i[2]));
3196  let total = getSum(data.map((i) => i[2]));
3197
3198  const svg = d3
3199    .select("#type_heatmap")
3200    // .append("svg")
3201    .insert("svg", ":first-child")
3202    .attr(
3203      "viewBox",
3204      `0 0  ${width + margin.left + margin.right} ${
3205        height + margin.top + margin.bottom
3206      }`
3207    )
3208    .attr("class", "pull-center")
3209    .style("display", "block")
3210    .style("margin-bottom", "10px")
3211    .append("g")
3212    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
3213
3214  let x = d3.scaleBand().range([0, width]).domain(xvals).padding(0.05);
3215  svg.append("g").call(d3.axisTop(x).tickSize(0)).select(".domain").remove();
3216
3217  let y = d3.scaleBand().range([0, height]).domain(yvals).padding(0.05);
3218  svg.append("g").call(d3.axisLeft(y).tickSize(0)).select(".domain").remove();
3219
3220  svg.call((s) => s.selectAll(".tick").attr("font-size", "8pt"));
3221
3222  // var myColor = d3.scaleSequential()
3223  //     .interpolator(d3.interpolateBlues)
3224  //     .domain([0, valmax*1.3])
3225
3226  var myColor = (d) => {
3227    let p = d3.interpolateRgb("#ffffff", "#6fcfd6");
3228    return p(d / valmax);
3229  };
3230
3231  svg
3232    .selectAll()
3233    .data(data)
3234    .enter()
3235    .append("rect")
3236    .attr("x", function (d) {
3237      return x(d[0]);
3238    })
3239    .attr("y", function (d) {
3240      return y(d[1]);
3241    })
3242    .attr("rx", 4)
3243    .attr("ry", 4)
3244    .attr("width", x.bandwidth())
3245    .attr("height", y.bandwidth())
3246    .style("fill", function (d) {
3247      return myColor(d[2]);
3248    })
3249    .style("stroke-width", 4)
3250    .style("stroke", "none")
3251    .style("opacity", 1);
3252
3253  let text = svg
3254    .selectAll()
3255    .data(data)
3256    .enter()
3257    .append("text")
3258    .attr("text-anchor", "middle")
3259    .attr("alignment-baseline", "middle")
3260    .attr("dominant-baseline", "middle")
3261    .attr("x", (d) => x(d[0]) + x.bandwidth() / 2)
3262    .attr("y", (d) => y(d[1]) + y.bandwidth() / 2)
3263    .attr("dy", 0)
3264    .attr("font-size", "12pt");
3265
3266  text
3267    .append("tspan")
3268    .text((d) => d[2])
3269    .attr("x", (d) => x(d[0]) + x.bandwidth() / 2)
3270    .attr("dy", 0);
3271  text
3272    .append("tspan")
3273    // .attr('x', 0)
3274    .attr("x", (d) => x(d[0]) + x.bandwidth() / 2)
3275    .attr("dy", 12)
3276    .text((d) => ((100 * d[2]) / total).toFixed(0) + "%")
3277    .attr("font-size", "6pt");
3278
3279  // Plot title
3280  // svg.append("text")
3281  //     .attr("text-anchor", "middle")
3282  //     .attr("x", width / 2)
3283  //     .attr("y", height + 5)
3284  //     .attr("font-size", "6pt")
3285  //     .text("Total points per price category and position");
3286
3287  let pos_totals = _(data)
3288    .groupBy(0)
3289    .map((i, v) => [v, _.sumBy(i, 2)])
3290    .value();
3291
3292  // Position sum
3293  let text2 = svg
3294    .selectAll()
3295    .data(pos_totals)
3296    .enter()
3297    .append("text")
3298    .attr("text-anchor", "middle")
3299    .attr("alignment-baseline", "middle")
3300    .attr("dominant-baseline", "middle")
3301    .attr("x", (d) => x(d[0]) + x.bandwidth() / 2)
3302    .attr("y", (d) => y("Total") + y.bandwidth() / 2)
3303    .attr("dy", 0)
3304    .attr("fill", "white")
3305    .attr("font-size", "12pt");
3306  text2
3307    .append("tspan")
3308    .text((d) => d[1])
3309    .attr("x", (d) => x(d[0]) + x.bandwidth() / 2)
3310    .attr("dy", 0);
3311  text2
3312    .append("tspan")
3313    // .attr('x', 0)
3314    .attr("x", (d) => x(d[0]) + x.bandwidth() / 2)
3315    .attr("dy", 12)
3316    .text((d) => ((100 * d[1]) / total).toFixed(1) + "%")
3317    .attr("font-size", "6pt");
3318
3319  // Price cat sum
3320  let bud_totals = _(data)
3321    .groupBy(1)
3322    .map((i, v) => [v, _.sumBy(i, 2)])
3323    .value();
3324
3325  let text3 = svg
3326    .selectAll()
3327    .data(bud_totals)
3328    .enter()
3329    .append("text")
3330    .attr("text-anchor", "middle")
3331    .attr("alignment-baseline", "middle")
3332    .attr("dominant-baseline", "middle")
3333    .attr("x", (d) => x("Total") + x.bandwidth() / 2)
3334    .attr("y", (d) => y(d[0]) + y.bandwidth() / 2)
3335    .attr("dy", 0)
3336    .attr("fill", "white")
3337    .attr("font-size", "12pt");
3338  text3
3339    .append("tspan")
3340    .text((d) => d[1])
3341    .attr("x", (d) => x("Total") + x.bandwidth() / 2)
3342    .attr("dy", 0);
3343  text3
3344    .append("tspan")
3345    // .attr('x', 0)
3346    .attr("x", (d) => x("Total") + x.bandwidth() / 2)
3347    .attr("dy", 12)
3348    .text((d) => ((100 * d[1]) / total).toFixed(1) + "%")
3349    .attr("font-size", "7pt");
3350
3351  svg
3352    .append("text")
3353    .attr("text-anchor", "middle")
3354    .attr("alignment-baseline", "middle")
3355    .attr("dominant-baseline", "middle")
3356    .attr("x", (d) => x("Total") + x.bandwidth() / 2)
3357    .attr("y", (d) => y("Total") + y.bandwidth() / 2)
3358    .attr("dy", 0)
3359    .attr("fill", "white")
3360    .attr("font-size", "12pt")
3361    .text(total);
3362}
3363
3364function draw_radar_map() {
3365  if (!app.is_ready) {
3366    return;
3367  }
3368
3369  const raw_width = 500;
3370  const raw_height = 500;
3371
3372  // bottom will be 40 when added new players
3373  const margin = { top: 10, right: 0, bottom: 10, left: 0 },
3374    width = raw_width - margin.left - margin.right,
3375    height = raw_height - margin.top - margin.bottom;
3376  let margin_common = 50;
3377
3378  let center = { x: raw_width / 2, y: raw_height / 2 };
3379
3380  document.getElementById("manager_comparison").innerHTML = "";
3381
3382  const svg = d3
3383    .select("#manager_comparison")
3384    .insert("svg", ":first-child")
3385    .attr(
3386      "viewBox",
3387      `0 0  ${width + margin.left + margin.right} ${
3388        height + margin.top + margin.bottom
3389      }`
3390    )
3391    .attr("class", "pull-center")
3392    .style("display", "block")
3393    .style("margin-bottom", "10px")
3394    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
3395
3396  let raw_data = app.user_picks_custom_stats;
3397  let data = [
3398    Object.entries(app.user_picks_custom_stats).map((i) => ({
3399      stat: i[0],
3400      value: (i[1].count / i[1].total) * 100,
3401    })),
3402  ];
3403  const names = [
3404    app.team_info.player_first_name + " " + app.team_info.player_last_name,
3405  ];
3406
3407  for (let r of app.rival_info) {
3408    names.push(r.info.player_first_name + " " + r.info.player_last_name);
3409    let picks = get_team_stats_picks(r.picks);
3410    data.push(
3411      Object.entries(picks).map((i) => ({
3412        stat: i[0],
3413        value: (i[1].count / i[1].total) * 100,
3414      }))
3415    );
3416  }
3417
3418  const maxvals = {
3419    "Clean Sheet": 60,
3420    "Def Con": 60,
3421    Goal: 60,
3422    Assist: 40,
3423    Bonus: 60,
3424    Captain: 70,
3425  };
3426
3427  data.forEach((d) => {
3428    d.forEach((s) => {
3429      s.perc = s.value / maxvals[s.stat];
3430    });
3431  });
3432
3433  let axesDomain = data[0].map((i) => i.stat);
3434
3435  let dotRadius = 4;
3436  let axisCircles = 5;
3437  let radius = (height - margin_common * 2) / 2;
3438  let axesLength = data[0].length;
3439  let angleSlice = (Math.PI * 2) / axesLength;
3440  let axisLabelFactor = 1.15;
3441  let maxValue = 1;
3442  let rScale = d3.scaleLinear().domain([0, maxValue]).range([0, radius]);
3443  let radarLine = d3
3444    .lineRadial()
3445    .curve(d3["curveLinearClosed"])
3446    .radius((d) => rScale(d))
3447    .angle((_, i) => i * angleSlice);
3448  let color = d3
3449    .scaleOrdinal()
3450    .range(["#EDC951", "#CC333F", "#00A0B0", "#00B055", "#71356D"]);
3451
3452  // let color = d3.scaleOrdinal(d3.schemeCategory10)
3453
3454  var axisGrid = svg.append("g").attr("class", "axisWrapper");
3455
3456  axisGrid
3457    .selectAll(".levels")
3458    .data(d3.range(1, axisCircles + 1).reverse())
3459    .enter()
3460    .append("circle")
3461    .attr("class", "gridCircle")
3462    .attr("cx", center.x)
3463    .attr("cy", center.y)
3464    .attr("r", (d, i) => (radius / axisCircles) * d)
3465    .style("fill", "#CDCDCD")
3466    .style("stroke", "#CDCDCD")
3467    .style("fill-opacity", 0.1);
3468
3469  const axis = axisGrid
3470    .selectAll(".axis")
3471    .data(axesDomain)
3472    .enter()
3473    .append("g")
3474    .attr("class", "axis");
3475
3476  axis
3477    .append("line")
3478    .attr("x1", center.x)
3479    .attr("y1", center.y)
3480    .attr(
3481      "x2",
3482      (d, i) =>
3483        rScale(maxValue * 1.1) * Math.cos(angleSlice * i - Math.PI / 2) +
3484        center.x
3485    )
3486    .attr(
3487      "y2",
3488      (d, i) =>
3489        rScale(maxValue * 1.1) * Math.sin(angleSlice * i - Math.PI / 2) +
3490        center.y
3491    )
3492    .attr("class", "line")
3493    .style("stroke", "#474747")
3494    .style("stroke-width", "2px");
3495
3496  axis
3497    .append("text")
3498    .attr("class", "legend text-with-shadow")
3499    .style("font-size", "12px")
3500    .attr("text-anchor", "middle")
3501    .attr("font-family", "sans-serif")
3502    .attr("fill", "white")
3503    .attr("dy", "0.35em")
3504    .attr(
3505      "x",
3506      (d, i) =>
3507        rScale(maxValue * axisLabelFactor) *
3508          Math.cos(angleSlice * i - Math.PI / 2) +
3509        center.x
3510    )
3511    .attr(
3512      "y",
3513      (d, i) =>
3514        rScale(maxValue * axisLabelFactor) *
3515          Math.sin(angleSlice * i - Math.PI / 2) +
3516        center.y
3517    )
3518    .text((d) => d);
3519
3520  axis
3521    .selectAll()
3522    // .data((d) => ({...d, 'c': d3.range(1, (axisCircles + 1)).reverse()}))
3523    // .enter()
3524    .data((d, j) =>
3525      d3.range(1, axisCircles + 1).map((i) => ({ i, d: d, j: j }))
3526    )
3527    .enter()
3528    .append("text")
3529    .attr(
3530      "x",
3531      (d) =>
3532        rScale((maxValue * d.i) / axisCircles) *
3533          Math.cos(angleSlice * d.j - Math.PI / 2) +
3534        center.x
3535    )
3536    .attr("text-anchor", "start")
3537    .attr(
3538      "y",
3539      (d) =>
3540        rScale((maxValue * d.i) / axisCircles) *
3541          Math.sin(angleSlice * d.j - Math.PI / 2) +
3542        center.y
3543    )
3544    .attr("alignment-baseline", "middle")
3545    .attr("dominant-baseline", "middle")
3546    .attr("fill", "white")
3547    .attr("font-size", "8px")
3548    .attr("class", "text-with-shadow")
3549    .text((d, i) => (d.i / axisCircles) * maxvals[d.d] + "%");
3550
3551  const plots = svg
3552    .append("g")
3553    .selectAll("g")
3554    .data(data)
3555    .join("g")
3556    .attr("data-name", (d, i) => names[i])
3557    .attr("fill", "none")
3558    .attr("stroke", "steelblue");
3559
3560  plots
3561    .append("path")
3562    .attr("d", (d) => radarLine(d.map((v) => v.perc)))
3563    .attr("fill", (d, i) => color(i))
3564    .attr("fill-opacity", 0.1)
3565    .attr("stroke", (d, i) => color(i))
3566    .attr("stroke-width", 2)
3567    .attr("transform", "translate(" + center.x + "," + center.y + ")");
3568
3569  plots
3570    .selectAll("circle")
3571    .data((d, j) => d.map((i) => ({ ...i, order: j })))
3572    .join("circle")
3573    .attr("r", dotRadius)
3574    .attr(
3575      "cx",
3576      (d, i) =>
3577        rScale(d.perc) * Math.cos(angleSlice * i - Math.PI / 2) + center.x
3578    )
3579    .attr(
3580      "cy",
3581      (d, i) =>
3582        rScale(d.perc) * Math.sin(angleSlice * i - Math.PI / 2) + center.y
3583    )
3584    .style("fill-opacity", 0.8)
3585    .attr("stroke", "none")
3586    .attr("fill", (d, i) => color(d.order));
3587
3588  let legend_y = raw_height - margin.bottom - 10;
3589
3590  let legend = svg.append("g");
3591  let props = {
3592    sep: 130,
3593    constant: 10,
3594    text_margin: 25,
3595    width: 20,
3596    height: 10,
3597  };
3598  let initial_x = (raw_width - data.length * props.sep) / 2;
3599
3600  legend
3601    .append("rect")
3602    .attr("x", initial_x - 10)
3603    .attr("y", legend_y - 10)
3604    .attr("width", data.length * props.sep + 20)
3605    .attr("height", props.height + 20)
3606    .attr("fill", "black")
3607    .attr("opacity", 0.4);
3608
3609  legend
3610    .selectAll()
3611    .data(names)
3612    .enter()
3613    .append("rect")
3614    .attr("x", (d, i) => props.sep * i + initial_x)
3615    .attr("y", legend_y)
3616    .attr("width", props.width)
3617    .attr("height", props.height)
3618    .attr("stroke", "white")
3619    .attr("fill", (d, i) => color(i));
3620
3621  legend
3622    .selectAll()
3623    .data(names)
3624    .enter()
3625    .append("text")
3626    .attr("x", (d, i) => props.sep * i + initial_x + props.text_margin)
3627    .attr("y", legend_y + props.height / 2)
3628    .attr("alignment-baseline", "middle")
3629    .attr("dominant-baseline", "middle")
3630    .attr("fill", "white")
3631    .attr("font-size", "10px")
3632    .attr("class", "white-shadow")
3633    .text((d, i) => d);
3634}
3635
3636function draw_event_heatmap() {
3637  if (!app.is_ready) {
3638    return;
3639  }
3640
3641  const raw_width = 500;
3642  const raw_height = 220;
3643
3644  const margin = { top: 20, right: 10, bottom: 20, left: 70 },
3645    width = raw_width - margin.left - margin.right,
3646    height = raw_height - margin.top - margin.bottom;
3647
3648  let raw_data = app.user_stats_per_gw.gw_grouped;
3649  let pt_data = app.user_stats_per_gw.points;
3650  let type_data = app.user_stats_per_gw.type_grouped;
3651  let overall_total = app.user_stats_per_gw.overall_total;
3652  // let gws = Object.keys(raw_data)
3653  let gws = d3.range(1, 39).map((i) => i.toString());
3654  let stats = Object.values(player_stat_types).map((i) => i.name);
3655  let xvals = gws.concat(["Total"]);
3656  let yvals = stats.concat(["Hits", "Total"]);
3657
3658  const svg = d3
3659    .select("#event_heatmap")
3660    // .append("svg")
3661    .insert("svg", ":first-child")
3662    .attr(
3663      "viewBox",
3664      `0 0  ${width + margin.left + margin.right} ${
3665        height + margin.top + margin.bottom
3666      }`
3667    )
3668    .attr("class", "pull-center")
3669    .style("display", "block")
3670    .style("margin-bottom", "10px")
3671    .append("g")
3672    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
3673
3674  let x = d3.scaleBand().range([0, width]).domain(xvals).padding(0.05);
3675  svg
3676    .append("g")
3677    .attr("transform", "translate(0," + height + ")")
3678    .attr("class", "x-axis")
3679    .call(d3.axisBottom(x).tickSize(0))
3680    .select(".domain")
3681    .remove();
3682  svg
3683    .append("g")
3684    .attr("class", "x-axis")
3685    .call(d3.axisTop(x).tickSize(0))
3686    .select(".domain")
3687    .remove();
3688
3689  let y = d3.scaleBand().range([0, height]).domain(yvals).padding(0.05);
3690  svg
3691    .append("g")
3692    .attr("class", "y-axis")
3693    .call(d3.axisLeft(y).tickSize(0))
3694    .select(".domain")
3695    .remove();
3696
3697  svg.call((s) => s.selectAll(".x-axis").attr("font-size", "4pt"));
3698  svg.call((s) => s.selectAll(".y-axis").attr("font-size", "5pt"));
3699
3700  // svg.call(s => s.selectAll(".tick").attr("font-size", "8pt"))
3701
3702  // var myColor = d3.scaleSequential()
3703  //     .interpolator(d3.interpolateBlues)
3704  //     .domain([0, valmax*1.3])
3705
3706  let stat_names = Object.keys(player_stat_types);
3707
3708  let data = [];
3709  for (let gw of gws) {
3710    for (let stat of stat_names) {
3711      let d =
3712        gw in raw_data ? (stat in raw_data[gw] ? raw_data[gw][stat] : []) : [];
3713      let sum = getSum(d.map((i) => i.total_points));
3714      data.push([gw, player_stat_types[stat].name, sum]);
3715    }
3716    data.push([gw, "Hits", gw - 1 in pt_data ? -pt_data[gw - 1].hit_pts : 0]);
3717    data.push([gw, "Total", gw - 1 in pt_data ? pt_data[gw - 1].net_pts : 0]);
3718  }
3719  for (let stat of stat_names) {
3720    let s = getSum(
3721      (type_data[stat] || []).map((i) => parseInt(i.points * i.multiplier))
3722    );
3723    data.push(["Total", player_stat_types[stat].name, s]);
3724  }
3725  // hit total
3726  let htot = -getSum(pt_data.map((i) => i.hit_pts));
3727  data.push(["Total", "Hits", htot]);
3728  data.push(["Total", "Total", overall_total]);
3729
3730  let valmax = Math.max(
3731    Math.max(
3732      ...data.filter((i) => i[1] != "Total" && i[0] != "Total").map((i) => i[2])
3733    ),
3734    1
3735  );
3736  let valmin = Math.min(
3737    ...data.filter((i) => i[1] != "Total" && i[0] != "Total").map((i) => i[2])
3738  );
3739  let gwmax = Math.max(
3740    Math.max(
3741      ...data.filter((i) => i[1] == "Total" && i[0] != "Total").map((i) => i[2])
3742    ),
3743    1
3744  );
3745  let gwmin = Math.min(
3746    ...data.filter((i) => i[1] == "Total" && i[0] != "Total").map((i) => i[2])
3747  );
3748  let stmax = Math.max(
3749    Math.max(
3750      ...data.filter((i) => i[0] == "Total" && i[1] != "Total").map((i) => i[2])
3751    ),
3752    1
3753  );
3754  let stmin = Math.min(
3755    ...data.filter((i) => i[0] == "Total" && i[1] != "Total").map((i) => i[2])
3756  );
3757
3758  var sColor = (d) => {
3759    let p = d3
3760      .scaleLinear()
3761      .domain([0, -valmin / (valmax - valmin), 1, 100])
3762      .range(["#e19797", "#ffffff", "#7FD3D9", "#7FD3D9"]);
3763    return p((d - valmin) / (valmax - valmin));
3764  };
3765  var gwColor = (d) => {
3766    let p = d3
3767      .scaleLinear()
3768      .domain([0, 1, 100])
3769      .range(["#ffffff", "#7FD3D9", "#7FD3D9"]);
3770    return p((d - gwmin) / (gwmax - gwmin));
3771  };
3772  var totColor = (d) => {
3773    let p = d3
3774      .scaleLinear()
3775      .domain([0, -stmin / (stmax - stmin), 1, 100])
3776      .range(["#e19797", "#ffffff", "#7FD3D9", "#7FD3D9"]);
3777    return p((d - stmin) / (stmax - stmin));
3778  };
3779
3780  detailed_point_cache = _.cloneDeep(data);
3781
3782  svg
3783    .selectAll()
3784    .data(data)
3785    .enter()
3786    .append("rect")
3787    .attr("x", function (d) {
3788      return x(d[0]);
3789    })
3790    .attr("y", function (d) {
3791      return y(d[1]);
3792    })
3793    .attr("rx", 2)
3794    .attr("ry", 2)
3795    .attr("width", x.bandwidth())
3796    .attr("height", y.bandwidth())
3797    .style("fill", (d) => {
3798      if (d[0] == "Total" && d[1] == "Total") {
3799        return "orange";
3800      } else if (d[0] == "Total") {
3801        return totColor(d[2]);
3802      } else if (d[1] == "Total") {
3803        return gwColor(d[2]);
3804      } else if (d[2] == 0) {
3805        return "darkgray";
3806      } else {
3807        return sColor(d[2]);
3808      }
3809    })
3810    .style("stroke-width", 4)
3811    .style("stroke", "none")
3812    .style("opacity", 1);
3813
3814  let text = svg
3815    .selectAll()
3816    .data(data)
3817    .enter()
3818    .append("text")
3819    .attr("text-anchor", "middle")
3820    .attr("alignment-baseline", "middle")
3821    .attr("dominant-baseline", "middle")
3822    .attr("x", (d) => x(d[0]) + x.bandwidth() / 2)
3823    .attr("y", (d) => y(d[1]) + y.bandwidth() / 2)
3824    .attr("dy", 0)
3825    .attr("font-size", "3.5pt")
3826    .text((d) => d[2]);
3827
3828  // Plot title
3829  svg
3830    .append("text")
3831    .attr("text-anchor", "middle")
3832    .attr("alignment-baseline", "hanging")
3833    .attr("dominant-baseline", "hanging")
3834    .attr("x", width / 2)
3835    .attr("y", height + 10)
3836    .attr("font-size", "5pt")
3837    .attr("fill", "white")
3838    .text("Total points per event type and GW");
3839}
3840
3841let refresh_y_axis;
3842
3843function draw_gain_loss_candlestick() {
3844  if (!app.is_ready) {
3845    return;
3846  }
3847
3848  let existing = document.getElementById("candlestick-chart-1");
3849  if (existing) existing.remove();
3850
3851  const raw_width = 500;
3852  const raw_height = 200;
3853
3854  const margin = { top: 20, right: 10, bottom: 20, left: 25 },
3855    width = raw_width - margin.left - margin.right,
3856    height = raw_height - margin.top - margin.bottom;
3857
3858  let data = _.cloneDeep(app.user_candlestick_values);
3859  let cum_sum = 0;
3860  data.forEach((e) => {
3861    e["before"] = cum_sum;
3862    e["after"] = cum_sum + e.net;
3863    if (e.part == 2) {
3864      cum_sum += e.net;
3865    }
3866  });
3867
3868  const raw_svg = d3
3869    .select("#benefit-candlestick-chart")
3870    .insert("svg", ":first-child")
3871    .attr("id", "candlestick-chart-1")
3872    .attr(
3873      "viewBox",
3874      `0 0  ${width + margin.left + margin.right} ${
3875        height + margin.top + margin.bottom
3876      }`
3877    )
3878    .attr("class", "pull-center")
3879    .style("display", "block")
3880    // basic zoom - disabled
3881    // .call(d3.zoom().on("zoom", function (event) {
3882    //     svg.attr("transform", event.transform)
3883    //  }))
3884    .style("margin-bottom", "10px");
3885
3886  raw_svg
3887    .append("defs")
3888    .append("SVG:clipPath")
3889    .attr("id", "clip")
3890    .append("SVG:rect")
3891    .attr("width", width)
3892    .attr("height", height)
3893    .attr("x", 0)
3894    .attr("y", 0);
3895
3896  let svg = raw_svg
3897    .append("g")
3898    .attr("class", "mainbg")
3899    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
3900
3901  svg
3902    .append("rect")
3903    .attr("id", "rect")
3904    .attr("width", width)
3905    .attr("height", height)
3906    .style("fill", "none")
3907    .style("pointer-events", "all")
3908    .attr("clip-path", "url(#clip)");
3909
3910  let chart_body = svg.append("g").attr("clip-path", "url(#clip)");
3911
3912  let xvals = _.range(1, 39);
3913  let x = d3.scaleBand().range([0, width]).domain(xvals).padding(0);
3914  svg
3915    .append("g")
3916    .attr("transform", "translate(0," + height + ")")
3917    .attr("class", "x-axis")
3918    .call(d3.axisBottom(x).tickSize(0));
3919
3920  let val_sum = [...data.map((i) => i.was), ...data.map((i) => i.current)];
3921  let max_y = Math.max(...val_sum) + 5;
3922  let min_y = Math.min(...val_sum) - 5;
3923
3924  let y = d3.scaleLinear().domain([min_y, max_y]).range([height, 0]);
3925  svg
3926    .append("g")
3927    .attr("class", "y-axis")
3928    .call(
3929      d3.axisRight(y).tickSize(width)
3930      //.tickValues(d3.range(min_y, max_y, 4))
3931    )
3932    .call((g) =>
3933      g
3934        .selectAll(".tick line")
3935        .attr("stroke-opacity", 0.2)
3936        .attr("stroke", "#9a9a9a")
3937        .attr("pointer-events", "none")
3938    )
3939    .call((g) =>
3940      g
3941        .selectAll(".tick text")
3942        .attr("x", -10)
3943        .attr("font-size", "5pt")
3944        .attr("fill", "#9a9a9a")
3945        .attr("text-anchor", "end")
3946    );
3947
3948  // axis style
3949  svg.call((g) => g.selectAll(".domain").attr("opacity", 0));
3950  svg.call((s) =>
3951    s.selectAll(".tick").attr("font-size", "5pt").attr("fill", "white")
3952  );
3953  svg.call((s) => s.selectAll(".tick text").attr("fill", "white"));
3954
3955  // gw backgrounds
3956  chart_body
3957    .selectAll()
3958    .data(xvals)
3959    .enter()
3960    .append("rect")
3961    .attr("width", x.bandwidth()) //x.bandwidth()/2 * 0.95)
3962    .attr("height", y(min_y) - y(max_y))
3963    .attr("x", (d) => x(d))
3964    .attr("y", y(max_y))
3965    .attr("class", (d) =>
3966      d % 2 == 0 ? "gw-bg gw-bg-even" : "gw-bg gw-bg-odd"
3967    );
3968
3969  // zero line
3970  let zero_line = chart_body
3971    .append("g")
3972    .append("line")
3973    .attr("x1", x(0))
3974    .attr("y1", y(0))
3975    .attr("x2", width)
3976    .attr("y2", y(0))
3977    .style("stroke", "white")
3978    .style("stroke-opacity", 0.5)
3979    .style("stroke-width", 1)
3980    .style("pointer-events", "none");
3981
3982  // extent = [[margin.left, margin.top], [width - margin.right, height-margin.top]]
3983  extent = [
3984    [0, 0],
3985    [width, height],
3986  ];
3987  let zoom = d3
3988    .zoom()
3989    .scaleExtent([1, 15])
3990    .translateExtent(extent)
3991    .extent(extent)
3992    .on("zoom", zoomin)
3993    .on("zoom.end", zoomend);
3994
3995  let perc_gap = 0.07;
3996
3997  let mouseover = (e, d) => hoverBar(e, d);
3998  let mousemove = (e, d) => hoverBar(e, d);
3999  let mouseleave = (e, d) => clearHover(e, d);
4000
4001  let candles = chart_body
4002    .selectAll()
4003    .data(data)
4004    .enter()
4005    .append("rect")
4006    .attr("width", (d) => x.bandwidth() * (0.5 - 2 * perc_gap)) //x.bandwidth()/2 * 0.95)
4007    .attr("height", (d) => Math.abs(y(d.current) - y(d.was)))
4008    .attr("x", (d) => {
4009      if (d.part == 1) {
4010        return x(d.gw) + x.bandwidth() * perc_gap;
4011      } else {
4012        return x(d.gw) + x.bandwidth() * (0.5 + perc_gap);
4013      }
4014    })
4015    .attr("y", (d) => y(Math.max(d.was, d.current)))
4016    .attr("class", (d) =>
4017      d.part == 1 ? "candle candle-gain" : "candle candle-loss"
4018    )
4019    .attr("opacity", 0.9)
4020    .on("mouseover", mouseover)
4021    .on("mousemove", mousemove)
4022    .on("mouseleave", mouseleave);
4023
4024  raw_svg.call(zoom);
4025
4026  function zoomin(event) {
4027    x.range([0, width].map((d) => event.transform.applyX(d)));
4028    svg
4029      .selectAll("rect.candle")
4030      .attr("x", (d) => {
4031        if (d.part == 1) {
4032          return x(d.gw) + x.bandwidth() * perc_gap;
4033        } else {
4034          return x(d.gw) + x.bandwidth() * (0.5 + perc_gap);
4035        }
4036      })
4037      .attr("width", (d) => x.bandwidth() * (0.5 - 2 * perc_gap));
4038    svg
4039      .selectAll("rect.gw-bg")
4040      .attr("x", (d) => x(d))
4041      .attr("width", x.bandwidth());
4042    svg.selectAll(".x-axis").call((g) =>
4043      g.attr("transform", "translate(0," + height + ")").call(
4044        d3
4045          .axisBottom(x)
4046          .tickSize(0)
4047          .tickFormat((d) => (x(d) + x.bandwidth() / 2 < 0 ? "" : d))
4048      )
4049    );
4050  }
4051
4052  let resizeTimer;
4053
4054  function zoomend(event) {
4055    var t = event.transform;
4056    clearTimeout(resizeTimer);
4057    resizeTimer = setTimeout(() => {
4058      refresh_y_axis();
4059    }, 300);
4060  }
4061
4062  function hoverBar(event, d) {
4063    document.getElementById("hover_gw").innerHTML = d.gw;
4064    document.getElementById("hover_gw_gain").innerHTML = getWithSign(d.gain);
4065    document.getElementById("hover_gw_loss").innerHTML = getWithSign(d.loss);
4066    let tg =
4067      '<span class="gain-blue">' +
4068      d.top_gains
4069        .map(
4070          (i) => app.fpl_element[i.id].web_name + " (" + rounded(i.net, 2) + ")"
4071        )
4072        .join("<br/>") +
4073      "</span>";
4074    let tl =
4075      '<span class="loss-red">' +
4076      d.top_losses
4077        .map(
4078          (i) => app.fpl_element[i.id].web_name + " (" + rounded(i.net, 2) + ")"
4079        )
4080        .join("<br/>") +
4081      "</span>";
4082    document.getElementById("hover_top_gains").innerHTML = tg;
4083    document.getElementById("hover_top_losses").innerHTML = tl;
4084    if (d.part == 1) {
4085      document
4086        .getElementById("hover_gw_gain")
4087        .parentElement.classList.add("highlighted-div");
4088      document
4089        .getElementById("hover_top_gains")
4090        .parentElement.classList.add("highlighted-div");
4091    } else if (d.part == 2) {
4092      document
4093        .getElementById("hover_gw_loss")
4094        .parentElement.classList.add("highlighted-div");
4095      document
4096        .getElementById("hover_top_losses")
4097        .parentElement.classList.add("highlighted-div");
4098    }
4099  }
4100
4101  function clearHover(event, d) {
4102    // document.getElementById("hover_gw").innerHTML = ''
4103    // document.getElementById("hover_gw_gain").innerHTML = ''
4104    // document.getElementById("hover_gw_loss").innerHTML = ''
4105    // document.getElementById("hover_top_gains").innerHTML = ''
4106    // document.getElementById("hover_top_losses").innerHTML = ''
4107    document
4108      .querySelectorAll(".highlighted-div")
4109      .forEach((e) => e.classList.remove("highlighted-div"));
4110  }
4111
4112  svg
4113    .append("text")
4114    .attr("text-anchor", "middle")
4115    .attr("x", width / 2)
4116    .attr("y", height + 20)
4117    .attr("font-size", "5pt")
4118    .attr("fill", "white")
4119    .text("Gameweeks");
4120
4121  // y-title
4122  svg
4123    .append("text")
4124    .attr("text-anchor", "start")
4125    .attr("x", -margin.left + 2)
4126    .attr("y", -3)
4127    .attr("font-size", "5pt")
4128    .attr("fill", "white")
4129    .text("Relative Change");
4130
4131  function set_new_yaxis() {
4132    let filtered = data.filter((d) => x(d.gw) >= 0 && x(d.gw) <= width);
4133    let cumulative = document.getElementById("cumulative_switch").checked;
4134    let net_ch = document.getElementById("net_switch").checked;
4135    let new_yvalues, new_max, new_min, y_func, h_func, o_func;
4136
4137    if (cumulative == true) {
4138      // default
4139      if (net_ch == false) {
4140        new_yvalues = filtered
4141          .map((i) => i.was)
4142          .concat(filtered.map((i) => i.current));
4143        y_func = (d) => y(Math.max(d.was, d.current));
4144        h_func = (d) => Math.abs(y(d.current) - y(d.was));
4145        o_func = (d) => 1;
4146      } else {
4147        new_yvalues = filtered
4148          .map((i) => i.before)
4149          .concat(filtered.map((i) => i.after));
4150        y_func = (d) => y(Math.max(d.before, d.after));
4151        h_func = (d) => Math.abs(y(d.after) - y(d.before));
4152        o_func = (d) =>
4153          (d.net >= 0 && d.part == 1) || (d.net < 0 && d.part == 2) ? 0.9 : 0;
4154      }
4155
4156      new_max = Math.max(...new_yvalues) + 5;
4157      new_min = Math.min(...new_yvalues) - 5;
4158    } else {
4159      // individual bars
4160      if (net_ch == false) {
4161        new_yvalues = filtered.map((i) => Math.max(i.current - i.was, 0));
4162        y_func = (d) => y(Math.abs(d.was - d.current));
4163        h_func = (d) => Math.abs(y(d.current) - y(d.was));
4164        o_func = (d) => 1;
4165      } else {
4166        new_yvalues = filtered.map((i) => i.net);
4167        y_func = (d) => y(Math.max(0, d.net));
4168        h_func = (d) => Math.abs(y(d.after) - y(d.before));
4169        o_func = (d) =>
4170          (d.net >= 0 && d.part == 1) || (d.net < 0 && d.part == 2) ? 0.9 : 0;
4171      }
4172
4173      new_max = Math.max(...new_yvalues) + 5;
4174      new_min = Math.min(...new_yvalues, 0) - 5;
4175    }
4176
4177    y.domain([new_min, new_max]);
4178    let yt = candles.transition().duration(400);
4179    yt.attr("y", (d) => y_func(d));
4180    yt.attr("height", (d) => h_func(d));
4181    yt.attr("opacity", (d) => o_func(d));
4182    yt.attr("pointer-events", (d) => (o_func(d) > 0.5 ? "auto" : "none"));
4183
4184    svg
4185      .selectAll(".y-axis")
4186      .transition()
4187      .duration(400)
4188      .call((g) => g.call(d3.axisRight(y).tickSize(width)))
4189      .call((g) =>
4190        g
4191          .selectAll(".tick line")
4192          .attr("stroke-opacity", 0.2)
4193          .attr("stroke", "#9a9a9a")
4194          .attr("pointer-events", "none")
4195      )
4196      .call((g) =>
4197        g
4198          .selectAll(".tick text")
4199          .attr("x", -10)
4200          .attr("font-size", "5pt")
4201          .attr("fill", "#9a9a9a")
4202          .attr("text-anchor", "end")
4203      );
4204    zero_line.transition().duration(400).attr("y1", y(0)).attr("y2", y(0));
4205
4206    return yt;
4207  }
4208
4209  function set_x_by_net(yt) {
4210    let net_ch = document.getElementById("net_switch").checked;
4211    if (net_ch == true) {
4212      yt.attr("x", (d) => x(d.gw) + x.bandwidth() * perc_gap).attr(
4213        "width",
4214        x.bandwidth() * (1 - 2 * perc_gap)
4215      );
4216    } else {
4217      yt.attr("x", (d) =>
4218        d.part == 1
4219          ? x(d.gw) + x.bandwidth() * perc_gap
4220          : x(d.gw) + x.bandwidth() * (0.5 + perc_gap)
4221      ).attr("width", x.bandwidth() * (0.5 - 2 * perc_gap));
4222    }
4223  }
4224
4225  let c_switch_timer = undefined;
4226  refresh_y_axis = () => {
4227    clearTimeout(c_switch_timer);
4228    c_switch_timer = setTimeout(() => {
4229      let yt = set_new_yaxis();
4230      set_x_by_net(yt);
4231    }, 100);
4232  };
4233
4234  refresh_y_axis();
4235}
4236
4237function draw_risk_reward_plot() {
4238  if (!app.is_ready) {
4239    return;
4240  }
4241
4242  let existing = document.getElementById("scatter-plot-1");
4243  if (existing) existing.remove();
4244
4245  const raw_width = 500;
4246  const raw_height = 300;
4247
4248  const margin = { top: 20, right: 10, bottom: 25, left: 25 },
4249    width = raw_width - margin.left - margin.right,
4250    height = raw_height - margin.top - margin.bottom;
4251
4252  let data = _.cloneDeep(app.user_ownership_gain_loss.combined_per_gw);
4253  let entries = data.map((i) => ({ gw: i[0], ...i[1] }));
4254
4255  // actual svg object
4256  const raw_svg = d3
4257    .select("#gw-risk-chart")
4258    .insert("svg", ":first-child")
4259    .attr("id", "scatter-plot-1")
4260    .attr(
4261      "viewBox",
4262      `0 0  ${width + margin.left + margin.right} ${
4263        height + margin.top + margin.bottom
4264      }`
4265    )
4266    .attr("class", "pull-center")
4267    .style("display", "block")
4268    .style("margin-bottom", "10px");
4269
4270  // clip object for zoom/transition effects
4271  raw_svg
4272    .append("defs")
4273    .append("SVG:clipPath")
4274    .attr("id", "clip2")
4275    .append("SVG:rect")
4276    .attr("width", width)
4277    .attr("height", height)
4278    .attr("x", 0)
4279    .attr("y", 0);
4280
4281  // x clip
4282  // raw_svg.append("clipPath")
4283  //     .attr('id', 'clip-x-axis')
4284  //     .append('rect')
4285  //     .attr('x', 0)
4286  //     .attr('y', 0)
4287  //     .attr('width', width)
4288  //     .attr('height', margin.bottom);
4289
4290  // // y clip
4291  // raw_svg.append("clipPath")
4292  //     .attr('id', 'clip-y-axis')
4293  //     .append('rect')
4294  //     .attr('x', - margin.left)
4295  //     .attr('y', 0)
4296  //     .attr('height', height)
4297  //     .attr('width', margin.left);
4298
4299  // main container -- everything lives on top of this
4300  let svg = raw_svg
4301    .append("g")
4302    .attr("class", "mainbg")
4303    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
4304  // hidden rect for interactivity
4305  svg
4306    .append("rect")
4307    .attr("id", "rect")
4308    .attr("width", width)
4309    .attr("height", height)
4310    .style("fill", "none")
4311    .style("pointer-events", "all")
4312    .attr("clip-path", "url(#clip)");
4313  // inside group that bars/nodes appear (to get clipped on zoom)
4314  let chart_body = svg.append("g").attr("clip-path", "url(#clip2)");
4315
4316  // x axis
4317  let max_x = 100;
4318  let min_x = 0;
4319  // let xvals = data.map(i => i.risk)
4320  let x = d3.scaleLinear().range([0, width]).domain([min_x, max_x]);
4321  let x_group = svg
4322    .append("g")
4323    // .attr('transform', 'translate(0,' + height + ')')
4324    .attr("class", "x-axis");
4325  // .attr('clip-path', 'url(#clip-x-axis)')
4326
4327  // y axis
4328  let yvals = entries.map((i) => i.net);
4329  let max_y = Math.ceil(Math.max(...yvals) / 5) * 5 + 5;
4330  let min_y = Math.floor(Math.min(...yvals) / 5) * 5 - 5;
4331
4332  let y = d3.scaleLinear().domain([min_y, max_y]).range([height, 0]);
4333  let y_group = svg.append("g").attr("class", "y-axis");
4334  // .attr('clip-path', 'url(#clip-y-axis)')
4335
4336  function setup_axes(x_in, y_in) {
4337    x_group.call(
4338      d3
4339        .axisBottom(x_in)
4340        .tickSize(height)
4341        .tickFormat((d) => d + "%")
4342    );
4343
4344    y_group
4345      .call(d3.axisRight(y_in).tickSize(width))
4346      .call((g) =>
4347        g.selectAll(".tick text").attr("x", -5).attr("text-anchor", "end")
4348      );
4349
4350    // axis style
4351    svg.call((g) => g.selectAll(".domain").attr("opacity", 0));
4352    svg.call((g) =>
4353      g
4354        .selectAll(".tick line")
4355        .attr("stroke-opacity", 0.2)
4356        .attr("stroke", "#9a9a9a")
4357        .attr("pointer-events", "none")
4358    );
4359    svg.call((s) =>
4360      s.selectAll(".tick").attr("font-size", "6pt").attr("fill", "white")
4361    );
4362    svg.call((s) => s.selectAll(".tick text").attr("fill", "white"));
4363  }
4364
4365  setup_axes(x, y);
4366
4367  // zero line
4368  let zero_line = chart_body
4369    .append("g")
4370    .append("line")
4371    .attr("x1", x(0))
4372    .attr("y1", y(0))
4373    .attr("x2", width)
4374    .attr("y2", y(0))
4375    .style("stroke", "white")
4376    .style("stroke-opacity", 0.5)
4377    .style("stroke-width", 1)
4378    .style("pointer-events", "none");
4379
4380  // zero line x
4381  let zero_line_2 = chart_body
4382    .append("g")
4383    .append("line")
4384    .attr("x1", x(50))
4385    .attr("y1", y(min_y))
4386    .attr("x2", x(50))
4387    .attr("y2", y(max_y))
4388    .style("stroke", "white")
4389    .style("stroke-opacity", 0.5)
4390    .style("stroke-width", 1)
4391    .style("pointer-events", "none");
4392
4393  let extent = [
4394    [0, 0],
4395    [width, height],
4396  ];
4397  let zoom = d3
4398    .zoom()
4399    .scaleExtent([1, 10])
4400    .translateExtent(extent)
4401    .extent(extent)
4402    .on("zoom", applyZoom);
4403  // .on('zoom.end', zoomend);
4404
4405  let mouseover = (e, d) => hoverRiskBar(e, d);
4406  let mousemove = (e, d) => hoverRiskBar(e, d);
4407
4408  let nodes = chart_body
4409    .selectAll()
4410    .data(entries)
4411    .enter()
4412    // .append('rect')
4413    // .attr("width", 6) //x.bandwidth()/2 * 0.95)
4414    // .attr("height", 6)
4415    // .attr("x", (d) => x(d.risk)-3)
4416    // .attr("y", (d) => y(d.net)-3)
4417    .append("circle")
4418    .attr("cx", (d) => x(d.risk))
4419    .attr("cy", (d) => y(d.net))
4420    .attr("r", 3)
4421    .attr("class", "risk-nodes")
4422    .attr("opacity", 0.9)
4423    .attr("fill", "orange")
4424    .on("mouseover", mouseover)
4425    .on("mousemove", mousemove);
4426  // .on("mouseleave", mouseleave);
4427
4428  function hoverRiskBar(event, d) {
4429    document.getElementById("risk_hover_gw").innerHTML = d.gw;
4430    document.getElementById("risk_hover_diff_rate").innerHTML =
4431      rounded(d.risk) + "%";
4432    document.getElementById("risk_hover_reward").innerHTML = getWithSign(d.net);
4433    let tg =
4434      '<span class="gain-blue">' +
4435      d.top_bets_for
4436        .map(
4437          (i) =>
4438            app.fpl_element[i.id].web_name +
4439            " (" +
4440            rounded(i.own_rate, 2) +
4441            "%)"
4442        )
4443        .join("<br/>") +
4444      "</span>";
4445    let tl =
4446      '<span class="loss-red">' +
4447      d.top_bets_against
4448        .map(
4449          (i) => app.fpl_element[i.id].web_name + " (" + rounded(i.eo, 2) + "%)"
4450        )
4451        .join("<br/>") +
4452      "</span>";
4453    document.getElementById("risk_top_bets_for").innerHTML = tg;
4454    document.getElementById("risk_top_bets_against").innerHTML = tl;
4455  }
4456
4457  svg
4458    .append("text")
4459    .attr("text-anchor", "middle")
4460    .attr("x", width / 2)
4461    .attr("y", height + 20)
4462    .attr("font-size", "5pt")
4463    .attr("fill", "white")
4464    .text("Risk (Differential) Percentage");
4465
4466  // y-title
4467  svg
4468    .append("text")
4469    .attr("text-anchor", "start")
4470    .attr("x", -margin.left + 2)
4471    .attr("y", -5)
4472    .attr("font-size", "5pt")
4473    .attr("fill", "white")
4474    .text("Relative Change");
4475
4476  function applyZoom(event) {
4477    const new_x = event.transform.rescaleX(x);
4478    const new_y = event.transform.rescaleY(y);
4479    setup_axes(new_x, new_y);
4480    nodes.attr("transform", event.transform); // you can redefine "r" if want the same radius
4481    zero_line.attr("transform", event.transform);
4482    zero_line_2.attr("transform", event.transform);
4483  }
4484
4485  raw_svg.call(zoom);
4486}
4487
4488function redraw_graphs() {
4489  draw_gain_loss_candlestick();
4490  draw_predicted_realized_diff();
4491  draw_risk_reward_plot();
4492  draw_team_season_visual();
4493  draw_point_origin_graph();
4494}
4495
4496let refresh_point_dist;
4497
4498function draw_tree_map() {
4499  const raw_width = 500;
4500  const raw_height = 400;
4501
4502  const margin = { top: 5, right: 20, bottom: 25, left: 20 },
4503    width = raw_width - margin.left - margin.right,
4504    height = raw_height - margin.top - margin.bottom;
4505
4506  let d1 = app.user_player_sum;
4507  let player_data = d1.filter((i) => i["points_total"] > 0);
4508
4509  if (_.isEmpty(player_data)) {
4510    return;
4511  }
4512
4513  let gain = app.user_ownership_gain_loss.gains;
4514  let player_gains = Object.fromEntries(
4515    _(gain)
4516      .groupBy("id")
4517      .map((i, v) => [v, getSum(i.map((j) => j.net))])
4518      .value()
4519  );
4520  player_data.forEach((e) => {
4521    e.value = e.points_total;
4522    e.pos = element_type[e.position].short;
4523    e.gain = player_gains[e.id] || 0;
4524  });
4525
4526  const svg = d3
4527    .select("#points_tree_map")
4528    .append("svg")
4529    .attr(
4530      "viewBox",
4531      `0 0  ${width + margin.left + margin.right} ${
4532        height + margin.top + margin.bottom
4533      }`
4534    )
4535    .attr("class", "pull-center")
4536    .style("display", "block")
4537    .style("margin-bottom", "10px")
4538    .append("g")
4539    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
4540
4541  let color_cat = d3
4542    .scaleOrdinal()
4543    .domain(["GK", "DF", "MD", "FW"])
4544    .range(["#ff8811", "#E1A1A7", "#CBC5EA", "#87B37A"]);
4545  var color = (c, e) => d3.interpolateRgb("#ffffff", color_cat(c))(e); // ff8811
4546
4547  // var opacity_by_pos = (d) => d3.scaleLinear()
4548  //     .domain([0, 200])
4549  //     .range([.4, 1])(d.data.value / d.data.pos)
4550
4551  // var opacity = d3.scaleLinear()
4552  //     //.domain([0, 200])
4553  //     .domain([0, 1])
4554  //     .range([.5, 1])
4555  var fontsize = d3.scaleLinear().domain([0, 1]).range([2, 6]);
4556
4557  // Plot title
4558  svg
4559    .append("text")
4560    .attr("text-anchor", "middle")
4561    .attr("x", width / 2)
4562    .attr("y", height + 15)
4563    .attr("font-size", "6pt")
4564    .text("Total points by position/player")
4565    .attr("fill", "white");
4566
4567  refresh_point_dist = () => {
4568    let gain_switch = false;
4569    try {
4570      gain_switch =
4571        document.getElementById("actual_gain_switch")?.checked || false;
4572    } catch {
4573      gain_switch = false;
4574    }
4575
4576    let sorted_data = undefined;
4577    let text_func, pos_text;
4578    if (gain_switch) {
4579      player_data.forEach((e) => {
4580        e.value = Math.max(0, e.gain);
4581      });
4582      group_max = Object.fromEntries(
4583        _(player_data)
4584          .groupBy("pos")
4585          .map((i, v) => [v, Math.max(...i.map((j) => j.gain))])
4586          .value()
4587      );
4588      player_data.forEach((e) => {
4589        e.ratio = e.gain / group_max[e.pos];
4590      });
4591      sorted_data = _(player_data).orderBy(["points_total"], ["desc"]).value();
4592      text_func = (d) => `${d.data.name} (${rounded(d.data.value)})`;
4593      pos_text = (d) => `${d.data.name} (${rounded(d.value)})`;
4594    } else {
4595      player_data.forEach((e) => {
4596        e.value = e.points_total;
4597      });
4598      group_max = Object.fromEntries(
4599        _(player_data)
4600          .groupBy("pos")
4601          .map((i, v) => [v, Math.max(...i.map((j) => j.points_total))])
4602          .value()
4603      );
4604      player_data.forEach((e) => {
4605        e.ratio = e.points_total / group_max[e.pos];
4606      });
4607      sorted_data = _(player_data).orderBy(["points_total"], ["desc"]).value();
4608      text_func = (d) => `${d.data.name} (${rounded(d.data.value)})`;
4609      pos_text = (d) => `${d.data.name} (${rounded(d.value)})`;
4610    }
4611
4612    let pos_data = _(sorted_data)
4613      .groupBy("pos")
4614      .map((i, v) => ({ name: v, children: i }))
4615      .value();
4616    pos_data = _.cloneDeep({ name: "main", children: pos_data });
4617
4618    // let pts_data = _(d1).groupBy("gw_no").map((gw,i) => [parseInt(i), gw.map(player => player.Contribution).reduce((a,b) =>
4618 parseInt(a)+parseInt(b), 0)]).value();
4619    let newroot = d3.hierarchy(pos_data).sum((d) => d.value);
4620
4621    d3
4622      .treemap()
4623      .size([width, height])
4624      .paddingTop(10)
4625      .paddingRight(3)
4626      .paddingInner(2)(newroot);
4627
4628    let newleaves = svg.selectAll(".tm-leaves").data(newroot.leaves());
4629    newleaves.exit().remove();
4630    newleaves
4631      .enter()
4632      .append("rect")
4633      .attr("class", "tm-leaves")
4634      .style("stroke", "black")
4635      .attr("x", 0)
4636      .attr("y", 0)
4637      .attr("width", 0)
4638      .attr("height", 0)
4639      .attr("fill", "black");
4640
4641    newleaves
4642      .transition()
4643      .duration(500)
4644      .attr("x", function (d) {
4645        return d.x0;
4646      })
4647      .attr("y", function (d) {
4648        return d.y0;
4649      })
4650      .attr("width", function (d) {
4651        return d.x1 - d.x0;
4652      })
4653      .attr("height", function (d) {
4654        return d.y1 - d.y0;
4655      })
4656      .style("fill", function (d) {
4657        return color(d.parent.data.name, d.data.ratio);
4658      });
4659
4660    let newtext = svg.selectAll(".tm-text").data(newroot.leaves());
4661    newtext.exit().attr("opacity", 1).text("").remove().attr("opacity", 0);
4662    newtext
4663      .enter()
4664      .append("text")
4665      .attr("class", "tm-text")
4666      .attr("text-anchor", "start")
4667      .attr("alignment-baseline", "hanging")
4668      .attr("dominant-baseline", "hanging")
4669      .attr("fill", "black")
4670      .text((d) => text_func(d));
4671    newtext
4672      .transition()
4673      .duration(500)
4674      .attr("x", (d) => d.x0 + 2)
4675      .attr("y", (d) => d.y0 + 2);
4676    svg
4677      .selectAll(".tm-text")
4678      .text((d) => text_func(d))
4679      .attr("font-size", (d) => fontsize(d.data.ratio) + "pt");
4680
4681    let newpostext = svg
4682      .selectAll(".pos-summary")
4683      .data(newroot.descendants().filter((d) => d.depth == 1));
4684    newpostext.exit().remove();
4685    newpostext
4686      .enter()
4687      .append("text")
4688      .attr("class", "pos-summary")
4689      .attr("fill", "white");
4690    newpostext
4691      .transition()
4692      .duration(500)
4693      .attr("x", function (d) {
4694        return d.x0;
4695      })
4696      .attr("y", function (d) {
4697        return d.y0 + 8;
4698      });
4699    svg
4700      .selectAll(".pos-summary")
4701      .text((d) => pos_text(d))
4702      .attr("font-size", "6pt");
4703  };
4704
4705  refresh_point_dist();
4706  refresh_point_dist();
4707}
4708
4709function draw_tree_map_loss() {
4710  const raw_width = 500;
4711  const raw_height = 400;
4712
4713  const margin = { top: 5, right: 20, bottom: 25, left: 20 },
4714    width = raw_width - margin.left - margin.right,
4715    height = raw_height - margin.top - margin.bottom;
4716
4717  let player_data = app.user_ownership_gain_loss.combined_per_player;
4718  if (player_data == undefined) {
4719    return;
4720  }
4721  player_data = player_data.filter((i) => i.loss > 1 || i.gain < 0);
4722  player_data = _.cloneDeep(player_data);
4723  let el_dict = app.fpl_element;
4724
4725  player_data.forEach((e) => {
4726    e.value = e.loss + (e.gain < 0 ? -e.gain : 0);
4727    e.position = el_dict[e.id].element_type;
4728    e.pos = element_type[e.position].short;
4729    e.name = el_dict[e.id].web_name;
4730  });
4731
4732  const svg = d3
4733    .select("#points_tree_map_loss")
4734    .append("svg")
4735    .attr(
4736      "viewBox",
4737      `0 0  ${width + margin.left + margin.right} ${
4738        height + margin.top + margin.bottom
4739      }`
4740    )
4741    .attr("class", "pull-center")
4742    .style("display", "block")
4743    .style("margin-bottom", "10px")
4744    .append("g")
4745    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
4746
4747  let color_cat = d3
4748    .scaleOrdinal()
4749    .domain(["GK", "DF", "MD", "FW"])
4750    .range(["#ff8811", "#E1A1A7", "#CBC5EA", "#87B37A"]);
4751  var color = (c, e) => d3.interpolateRgb("#ffffff", color_cat(c))(e); // ff8811
4752
4753  // var opacity_by_pos = (d) => d3.scaleLinear()
4754  //     .domain([0, 200])
4755  //     .range([.4, 1])(d.data.value / d.data.pos)
4756
4757  // var opacity = d3.scaleLinear()
4758  //     //.domain([0, 200])
4759  //     .domain([0, 1])
4760  //     .range([.5, 1])
4761  var fontsize = d3.scaleLinear().domain([0, 1]).range([2, 6]);
4762
4763  // Plot title
4764  svg
4765    .append("text")
4766    .attr("text-anchor", "middle")
4767    .attr("x", width / 2)
4768    .attr("y", height + 15)
4769    .attr("font-size", "6pt")
4770    .text("Relative loss by position/player")
4771    .attr("fill", "white");
4772
4773  let sorted_data = undefined;
4774  let text_func, pos_text;
4775
4776  plot_now = () => {
4777    group_max = Object.fromEntries(
4778      _(player_data)
4779        .groupBy("pos")
4780        .map((i, v) => [v, Math.max(...i.map((j) => j.loss))])
4781        .value()
4782    );
4783    player_data.forEach((e) => {
4784      e.ratio = e.loss / group_max[e.pos];
4785    });
4786    sorted_data = _(player_data).orderBy(["loss"], ["desc"]).value(
4786);
4787    text_func = (d) => `${d.data.name} (-${rounded(d.data.value)})`;
4788    pos_text = (d) => `${d.data.name} (-${rounded(d.value)})`;
4789
4790    let pos_data = _(sorted_data)
4791      .groupBy("pos")
4792      .map((i, v) => ({ name: v, children: i }))
4793      .value();
4794    pos_data = _.cloneDeep({ name: "main", children: pos_data });
4795
4796    // let pts_data = _(d1).groupBy("gw_no").map((gw,i) => [parseInt(i), gw.map(player => player.Contribution).reduce((a,b) => parseInt(a)+parseInt(b), 0)]).value();
4797    let newroot = d3.hierarchy(pos_data).sum((d) => d.value);
4798
4799    d3
4800      .treemap()
4801      .size([width, height])
4802      .paddingTop(10)
4803      .paddingRight(3)
4804      .paddingInner(2)(newroot);
4805
4806    let newleaves = svg.selectAll(".tm-leaves").data(newroot.leaves());
4807    newleaves.exit().remove();
4808    newleaves
4809      .enter()
4810      .append("rect")
4811      .attr("class", "tm-leaves")
4812      .style("stroke", "black")
4813      .attr("x", 0)
4814      .attr("y", 0)
4815      .attr("width", 0)
4816      .attr("height", 0)
4817      .attr("fill", "black");
4818
4819    newleaves
4820      .transition()
4821      .duration(500)
4822      .attr("x", function (d) {
4823        return d.x0;
4824      })
4825      .attr("y", function (d) {
4826        return d.y0;
4827      })
4828      .attr("width", function (d) {
4829        return d.x1 - d.x0;
4830      })
4831      .attr("height", function (d) {
4832        return d.y1 - d.y0;
4833      })
4834      .style("fill", function (d) {
4835        return color(d.parent.data.name, d.data.ratio);
4836      });
4837
4838    let newtext = svg.selectAll(".tm-text").data(newroot.leaves());
4839    newtext.exit().attr("opacity", 1).text("").remove().attr("opacity", 0);
4840    newtext
4841      .enter()
4842      .append("text")
4843      .attr("class", "tm-text")
4844      .attr("text-anchor", "start")
4845      .attr("alignment-baseline", "hanging")
4846      .attr("dominant-baseline", "hanging")
4847      .attr("fill", "black")
4848      .text((d) => text_func(d));
4849    newtext
4850      .transition()
4851      .duration(500)
4852      .attr("x", (d) => d.x0 + 2)
4853      .attr("y", (d) => d.y0 + 2);
4854    svg
4855      .selectAll(".tm-text")
4856      .text((d) => text_func(d))
4857      .attr("font-size", (d) => fontsize(d.data.ratio) + "pt");
4858
4859    let newpostext = svg
4860      .selectAll(".pos-summary")
4861      .data(newroot.descendants().filter((d) => d.depth == 1));
4862    newpostext.exit().remove();
4863    newpostext
4864      .enter()
4865      .append("text")
4866      .attr("class", "pos-summary")
4867      .attr("fill", "white");
4868    newpostext
4869      .transition()
4870      .duration(500)
4871      .attr("x", function (d) {
4872        return d.x0;
4873      })
4874      .attr("y", function (d) {
4875        return d.y0 + 8;
4876      });
4877    svg
4878      .selectAll(".pos-summary")
4879      .text((d) => pos_text(d))
4880      .attr("font-size", "6pt");
4881  };
4882
4883  plot_now();
4884  plot_now();
4885}
4886
4887function draw_predicted_realized_diff() {
4888  // season-xp-real-diff-plot
4889
4890  if (!app.is_ready) {
4891    return;
4892  }
4893
4894  const raw_width = 500;
4895  const raw_height = 200;
4896
4897  const margin = { top: 20, right: 10, bottom: 20, left: 25 },
4898    width = raw_width - margin.left - margin.right,
4899    height = raw_height - margin.top - margin.bottom;
4900
4901  let data = _.cloneDeep(app.user_gw_results);
4902
4903  jQuery("#season-xp-real-diff-plot").empty();
4904
4905  const raw_svg = d3
4906    .select("#season-xp-real-diff-plot")
4907    .append("svg")
4908    .attr(
4909      "viewBox",
4910      `0 0  ${width + margin.left + margin.right} ${
4911        height + margin.top + margin.bottom
4912      }`
4913    )
4914    .attr("class", "pull-center")
4915    .style("display", "block")
4916    .style("margin-bottom", "10px");
4917
4918  let svg = raw_svg
4919    .append("g")
4920    .attr("class", "mainbg")
4921    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
4922
4923  let xvals = _.range(1, 39);
4924  let x = d3.scaleBand().range([0, width]).domain(xvals).padding(0);
4925  svg
4926    .append("g")
4927    .attr("transform", "translate(0," + height + ")")
4928    .attr("class", "x-axis")
4929    .call(d3.axisBottom(x).tickSize(0));
4930
4931  let values = Object.values(data.data);
4932  let val_sum = [
4933    0,
4934    ...values.map((i) => i.total_exp_diff),
4935    ...values.map((i) => i.total_real_diff),
4936  ];
4937  let max_y = Math.max(...val_sum) + 10;
4938  let min_y = Math.min(...val_sum) - 10;
4939
4940  let y = d3.scaleLinear().domain([min_y, max_y]).range([height, 0]);
4941  svg
4942    .append("g")
4943    .attr("class", "y-axis")
4944    .call(
4945      d3
4946        .axisRight(y)
4947        .tickSize(width)
4948        .tickFormat((v) => (v > 0 ? "+" + v : v))
4949      //.tickValues(d3.range(min_y, max_y, 4))
4950    )
4951    .call((g) =>
4952      g
4953        .selectAll(".tick line")
4954        .attr("stroke-opacity", 0.2)
4955        .attr("stroke", "#9a9a9a")
4956        .attr("pointer-events", "none")
4957    )
4958    .call((g) =>
4959      g
4960        .selectAll(".tick text")
4961        .attr("x", -5)
4962        .attr("font-size", "5pt")
4963        .attr("fill", "#9a9a9a")
4964        .attr("text-anchor", "end")
4965    );
4966
4967  // axis style
4968  svg.call((g) => g.selectAll(".domain").attr("opacity", 0));
4969  svg.call((s) =>
4970    s.selectAll(".tick").attr("font-size", "5pt").attr("fill", "white")
4971  );
4972  svg.call((s) => s.selectAll(".tick text").attr("fill", "white"));
4973
4974  // gw backgrounds
4975  svg
4976    .selectAll()
4977    .data(xvals)
4978    .enter()
4979    .append("rect")
4980    .attr("width", x.bandwidth()) //x.bandwidth()/2 * 0.95)
4981    .attr("height", y(min_y) - y(max_y))
4982    .attr("x", (d) => x(d))
4983    .attr("y", y(max_y))
4984    .attr("class", (d) =>
4985      d % 2 == 0 ? "gw-bg gw-bg-even" : "gw-bg gw-bg-odd"
4986    );
4987
4988  // zero & 100 lines
4989
4990  let markers = _.range(
4991    Math.min(100 * Math.ceil(min_y / 100), 0),
4992    max_y,
4993    100
4994  ).filter((i) => i != 0);
4995
4996  let zero_line = svg
4997    .append("g")
4998    .append("line")
4999    .attr("x1", x(0))
5000    .attr("y1", y(0))
5001    .attr("x2", width)
5002    .attr("y2", y(0))
5003    .style("stroke", "white")
5004    .style("stroke-opacity", 0.5)
5005    .style("stroke-width", 1)
5006    .style("pointer-events", "none");
5007
5008  let hundred_lines = svg
5009    .append("g")
5010    .selectAll()
5011    .data(markers)
5012    .enter()
5013    .append("line")
5014    .attr("x1", x(0))
5015    .attr("y1", (d) => y(d))
5016    .attr("x2", width)
5017    .attr("y2", (d) => y(d))
5018    .style("stroke", "#f32d7a")
5019    .style("stroke-opacity", 0.2)
5020    .style("stroke-width", 1)
5021    .style("pointer-events", "none");
5022
5023  if (markers.includes(-100)) {
5024    svg
5025      .append("g")
5026      .append("text")
5027      .attr("text-anchor", "end")
5028      .attr("alignment-baseline", "after-edge")
5029      .attr("dominant-baseline", "after-edge")
5030      .attr("x", width)
5031      .attr("y", y(-100))
5032      .attr("font-size", "5pt")
5033      .attr("fill", "#a78694")
5034      .text("EuroFPL/Formadillo Line");
5035  }
5036
5037  // prediction line
5038  let pred_data = _.orderBy(Object.values(data.data), "gw", "asc");
5039
5040  pred_data = [{ gw: 0, total_exp_diff: 0, total_real_diff: 0 }, ...pred_data];
5041
5042  // luck regions
5043
5044  svg
5045    .append("clipPath")
5046    .attr("id", `below-exp`)
5047    .append("path")
5048    .datum(pred_data)
5049    .attr(
5050      "d",
5051      d3
5052        .area()
5053        .x((d) => (d.gw == 38 ? x(d.gw) + x.bandwidth() : x(d.gw + 1)))
5054        .y1((d) => y(d.total_exp_diff))
5055        .y0((d) => height)
5056    );
5057
5058  svg
5059    .append("clipPath")
5060    .attr("id", `above-exp`)
5061    .append("path")
5062    .datum(pred_data)
5063    .attr(
5064      "d",
5065      d3
5066        .area()
5067        .x((d) => (d.gw == 38 ? x(d.gw) + x.bandwidth() : x(d.gw + 1)))
5068        .y1((d) => y(d.total_exp_diff))
5069        .y0((d) => 0)
5070    );
5071
5072  svg
5073    .append("g")
5074    .append("path")
5075    .datum(pred_data)
5076    .attr("fill", "#ff8da1")
5077    .attr("fill-opacity", 0.1)
5078    .attr("clip-path", "url(#below-exp)")
5079    .attr(
5080      "d",
5081      d3
5082        .area()
5083        .x((d) => (d.gw == 38 ? x(d.gw) + x.bandwidth() : x(d.gw + 1)))
5084        .y1((d) => y(d.total_real_diff))
5085        .y0((d) => 0)
5086    );
5087
5088  svg
5089    .append("g")
5090    .append("path")
5091    .datum(pred_data)
5092    .attr("fill", "#008b6d")
5093    .attr("fill-opacity", 0.1)
5094    .attr("clip-path", "url(#above-exp)")
5095    .attr(
5096      "d",
5097      d3
5098        .area()
5099        .x((d) => (d.gw == 38 ? x(d.gw) + x.bandwidth() : x(d.gw + 1)))
5100        .y1((d) => y(d.total_real_diff))
5101        .y0((d) => height)
5102    );
5103
5104  // expected values line
5105  svg
5106    .append("g")
5107    .append("path")
5108    .datum(pred_data)
5109    .attr("fill", "none")
5110    .attr("stroke", "#dbf7b4")
5111    .attr("stroke-opacity", 0.8)
5112    .style("stroke-dasharray", "2,0.5")
5113    .attr("stroke-width", 1)
5114    .style("pointer-events", "none")
5115    .attr(
5116      "d",
5117      d3
5118        .line()
5119        .x((d) => (d.gw == 38 ? x(d.gw) + x.bandwidth() : x(d.gw + 1)))
5120        .y((d) => y(d.total_exp_diff))
5121    );
5122
5123  // realized values line
5124  svg
5125    .append("g")
5126    .append("path")
5127    .datum(pred_data)
5128    .attr("fill", "none")
5129    .attr("stroke", "#6fcfd6")
5130    .attr("stroke-opacity", 0.8)
5131    .attr("stroke-width", 1)
5132    .style("pointer-events", "none")
5133    .attr(
5134      "d",
5135      d3
5136        .line()
5137        .x((d) => (d.gw == 38 ? x(d.gw) + x.bandwidth() : x(d.gw + 1)))
5138        .y((d) => y(d.total_real_diff))
5139    );
5140
5141  // axis titles
5142  let titles = svg.append("g");
5143  titles
5144    .append("text")
5145    .attr("text-anchor", "middle")
5146    .attr("x", width / 2)
5147    .attr("y", height + 17)
5148    .attr("font-size", "5pt")
5149    .attr("fill", "white")
5150    .text("Gameweeks");
5151
5152  titles
5153    .append("text")
5154    .attr("text-anchor", "end")
5155    .attr("x", -5)
5156    .attr("y", -5)
5157    .attr("font-size", "5pt")
5158    .attr("fill", "white")
5159    .text("Diff.");
5160
5161  titles
5162    .append("text")
5163    .attr("text-anchor", "middle")
5164    .attr("x", width / 2)
5165    .attr("y", -7)
5166    .attr("font-size", "5pt")
5167    .attr("fill", "white")
5168    .text(
5169      `Difference to Tier Average (${app.sample_label(
5170        app.sample_options[app.sample_selection]
5171      )})`
5172    );
5173
5174  titles
5175    .append("foreignObject")
5176    .attr("width", 120)
5177    .attr("height", 50)
5178    .attr("x", 410)
5179    .attr("y", -15)
5180    .append("xhtml:div")
5181    .style("font-size", "6px")
5182    .html(
5183      `<img src="static/images/logo_128.png" width="12"><span style="margin-left: 2px;">FPL Optimized</span>`
5184    );
5185
5186  // titles.append('text')
5187  //     .attr("text-anchor", "end")
5188  //     .attr("alignment-baseline", "middle")
5189  //     .attr("dominant-baseline", "middle")
5190  //     .attr("x", width-2)
5191  //     .attr("y", -7)
5192  //     .attr("font-size", "5pt")
5193  //     .attr("fill", "#ffc100")
5194  //     .text(`${app.team_info.name} (${app.team_info.id})`);
5195
5196  let chips = _.toPairs(app.team_data)
5197    .map((i) => [i[0], i[1].active_chip])
5198    .filter((i) => i[1] != null);
5199  let chip_dict = { wildcard: "WC", "3xc": "TC", freehit: "FH", bboost: "BB" };
5200  svg
5201    .append("g")
5202    .selectAll()
5203    .data(chips)
5204    .enter()
5205    .append("text")
5206    .attr("text-anchor", "middle")
5207    .attr("alignment-baseline", "hanging")
5208    .attr("dominant-baseline", "hanging")
5209    .attr("x", (d) => x(d[0]) + x.bandwidth() / 2)
5210    .attr("y", 2)
5211    .attr("font-size", "4pt")
5212    .attr("fill", "white")
5213    .attr("fill-opacity", "0.4")
5214    .text((d) => chip_dict[d[1]]);
5215}
5216
5217function draw_team_season_visual() {
5218  if (!app.is_ready) {
5219    return;
5220  }
5221
5222  const raw_width = 600;
5223  const raw_height = 20 * (parseInt(app.next_gw) + 1); //400;
5224
5225  const margin = { top: 20, right: 10, bottom: 20, left: 15 },
5226    width = raw_width - margin.left - margin.right,
5227    height = raw_height - margin.top - margin.bottom;
5228
5229  let data = _.cloneDeep(app.user_picks_with_order);
5230
5231  let gws = _.sortedUniq(data.map((i) => parseInt(i.gw)));
5232
5233  jQuery("#team_season_visual").empty();
5234
5235  const raw_svg = d3
5236    .select("#team_season_visual")
5237    .append("svg")
5238    .attr(
5239      "viewBox",
5240      `0 0  ${width + margin.left + margin.right} ${
5241        height + margin.top + margin.bottom
5242      }`
5243    )
5244    .attr("class", "pull-center")
5245    .style("display", "block")
5246    .style("margin-bottom", "10px");
5247
5248  let svg = raw_svg
5249    .append("g")
5250    .attr("class", "mainbg")
5251    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
5252
5253  let p2_left = 0;
5254  if (app.show_info_ts) {
5255    p2_left = width / 8;
5256  }
5257
5258  let use_team_colors = app.use_team_colors;
5259  if (use_team_colors) {
5260    let fpl_dict = app.fpl_element;
5261    data.forEach((p) => {
5262      let team = team_codes[fpl_dict[p.element].team_code].short;
5263      p.bg_color = pl_team_colors[team][0];
5264      p.fg_color = pl_team_colors[team][1];
5265    });
5266  }
5267
5268  let xvals = _.range(1, 17);
5269  let x = d3
5270    .scaleBand()
5271    .range([p2_left, width])
5272    .domain(xvals)
5273    .paddingInner(0.1)
5274    .paddingOuter(0);
5275  svg
5276    .append("g")
5277    // .attr('transform', 'translate(0,' + height + ')')
5278    .attr("class", "x-axis")
5279    .call(d3.axisTop(x).tickSize(0))
5280    .call((g) =>
5281      g.selectAll(".tick text").attr("opacity", 0).style("display", "none")
5282    );
5283
5284  let x2;
5285  if (app.show_info_ts) {
5286    let x2vals = ["Chip", "Pts", "OR"];
5287    x2 = d3
5288      .scaleBand()
5289      .range([0, p2_left])
5290      .domain(x2vals)
5291      .paddingInner(0.1)
5292      .paddingOuter(0);
5293    svg
5294      .append("g")
5295      // .attr('transform', 'translate(0,-8)')
5296      .attr("class", "x-axis-2")
5297      .call(d3.axisTop(x2).tickSize(0));
5298    svg.call((g) =>
5299      g.selectAll(".x-axis-2").style("transform", "translate(0px, -3px)")
5300    );
5301  }
5302
5303  // .call(g => g.selectAll(".tick text")
5304  //     .attr("opacity", 0)
5305  //     .style("display", "none")
5306  // )
5307
5308  let y_vals = gws;
5309  let y = d3
5310    .scaleBand()
5311    .domain(_.reverse(y_vals))
5312    .range([height, 0])
5313    .paddingInner(0.1)
5314    .paddingOuter(0);
5315  svg
5316    .append("g")
5317    .attr("class", "y-axis")
5318    .call(d3.axisLeft(y).tickSize(0))
5319    .call((g) =>
5320      g
5321        .selectAll(".tick line")
5322        .attr("stroke-opacity", 0.2)
5323        .attr("stroke", "#9a9a9a")
5324        .attr("pointer-events", "none")
5325    )
5326    .call((g) =>
5327      g
5328        .selectAll(".tick text")
5329        .attr("x", -5)
5330        .attr("font-size", "5pt")
5331        .attr("fill", "#9a9a9a")
5332        .attr("text-anchor", "end")
5333    );
5334
5335  // svg settings
5336  svg.call((g) => g.selectAll(".domain").attr("opacity", 0));
5337  svg.call((s) =>
5338    s.selectAll(".tick").attr("font-size", "5pt").attr("fill", "white")
5339  );
5340  svg.call((s) => s.selectAll(".tick text").attr("fill", "white"));
5341
5342  let body = svg.append("g");
5343
5344  // pos plot
5345
5346  let pos_data = [
5347    { name: "GK", start: 1, finish: 2 },
5348    { name: "DF", start: 3, finish: 7 },
5349    { name: "MD", start: 8, finish: 12 },
5350    { name: "FW", start: 13, finish: 15 },
5351  ];
5352
5353  let pos_grp = body.append("g").selectAll().data(pos_data).enter();
5354
5355  pos_grp
5356    .append("rect")
5357    .attr("width", (d) => x(d.finish) - x(d.start) + x.bandwidth())
5358    .attr("height", (d) => y.bandwidth())
5359    .attr("x", (d) => x(d.start))
5360    .attr("y", -y.bandwidth() - 4)
5361    .attr("class", "team_pos_box");
5362
5363  pos_grp
5364    .append("text")
5365    .attr("text-anchor", "middle")
5366    .attr("alignment-baseline", "middle")
5367    .attr("dominant-baseline", "middle")
5368    .attr("x", (d) => (x(d.start) + x(d.finish)) / 2 + x.bandwidth() / 2)
5369    .attr("y", (d) => -y.bandwidth() / 2 - 4 + 1)
5370    .attr("font-size", "4pt")
5371    .attr("class", "team_pos_name")
5372    .text((d) => d.name);
5373
5374  // data plot
5375
5376  let actual_markers = data.filter((i) => i.sort_finish);
5377
5378  let markers = body.append("g").selectAll().data(actual_markers).enter();
5379
5380  markers
5381    .append("rect")
5382    .attr("width", x.bandwidth()) //x.bandwidth()/2 * 0.95)
5383    .attr("height", (d) => y(d.sort_finish) - y(d.sort_start) + y.bandwidth())
5384    .attr("x", (d) => x(d.sort_order))
5385    .attr("y", (d) => y(d.sort_start))
5386    .attr("class", "team_visual_box")
5387    .style("fill", (d) => (use_team_colors ? d.bg_color : ""))
5388    .style("stroke", (d) => (use_team_colors ? d.fg_color : ""))
5389    .style("stroke-width", (d) => (use_team_colors ? "0.5px" : ""));
5390
5391  let gw_markers = body.append("g").selectAll().data(data).enter();
5392
5393  if (app.show_pts_on_ts) {
5394    let bColor = (v) => {
5395      if (!use_team_colors) {
5396        let p = d3
5397          .scaleLinear()
5398          .domain([-100, 3, 4, 20, 100])
5399          .range(["#ff9c99", "#ff9c99", "#2cf5ff", "#49c6ff", "#49c6ff"]);
5400        return p(v);
5401      } else {
5402        let p = d3
5403          .scaleLinear()
5404          .domain([-100, 3, 4, 20, 100])
5405          .range(["#ffc5c5", "#ffc5c5", "#ddfbff", "#ddfbff", "#ddfbff"]);
5406        return p(v);
5407      }
5408    };
5409
5410    gw_markers
5411      .append("rect")
5412      .attr("width", x.bandwidth() / 6)
5413      .attr("height", (d) => y.bandwidth())
5414      .attr("x", (d) => x(d.sort_order) + (x.bandwidth() * 5) / 6)
5415      .attr("y", (d) => y(d.gw))
5416      .attr("class", "gw_visual_box")
5417      .attr("fill", (d) => (d.multiplier == 0 ? "none" : bColor(d.rp)));
5418  }
5419
5420  let text_group = body.append("g").selectAll().data(actual_markers).enter();
5421
5422  text_group
5423    .append("text")
5424    .attr("text-anchor", "middle")
5425    .attr("alignment-baseline", "middle")
5426    .attr("dominant-baseline", "middle")
5427    .attr(
5428      "x",
5429      (d) =>
5430        x(d.sort_order) +
5431        (app.show_pts_on_ts ? (x.bandwidth() * 5) / 12 : x.bandwidth() / 2)
5432    )
5433    .attr(
5434      "y",
5435      (d) => (y(d.sort_finish) + y(d.sort_start)) / 2 + y.bandwidth() / 2 + 1
5436    )
5437    .attr("font-size", "4pt")
5438    // .attr("fill", "black")
5439    // .attr("fill", "#ffc100")
5440    .attr("class", "team_box_player_name")
5441    .style("fill", (d) => (use_team_colors ? d.fg_color : ""))
5442    .text((d) => long_name_str(app.fpl_element[d.element].web_name));
5443
5444  if (!use_team_colors) {
5445    text_group
5446      .append("rect")
5447      .attr("width", x.bandwidth()) //x.bandwidth()/2 * 0.95)
5448      .attr("height", (d) => y(d.sort_finish) - y(d.sort_start) + y.bandwidth())
5449      .attr("x", (d) => x(d.sort_order))
5450      .attr("y", (d) => y(d.sort_start))
5451      .attr("class", "team_visual_border");
5452  }
5453
5454  if (app.show_pts_on_ts) {
5455    // captain square
5456    body
5457      .append("g")
5458      .selectAll()
5459      .data(data.filter((i) => i.multiplier > 1))
5460      .enter()
5461      .append("rect")
5462      .attr("width", x.bandwidth() / 6)
5463      .attr("height", (d) => y.bandwidth())
5464      .attr("x", (d) => x(d.sort_order) + (x.bandwidth() * 5) / 6)
5465      .attr("y", (d) => y(d.gw))
5466      .attr("class", "team_captain_border");
5467
5468    body
5469      .append("g")
5470      .selectAll()
5471      .data(data)
5472      .enter()
5473      .append("text")
5474      .attr("text-anchor", "middle")
5475      .attr("alignment-baseline", "middle")
5476      .attr("dominant-baseline", "middle")
5477      .attr("x", (d) => x(d.sort_order) + (x.bandwidth() * 11) / 12)
5478      .attr("y", (d) => y(d.gw) + y.bandwidth() / 2 + 1)
5479      .attr("font-size", "4pt")
5480      // .attr("fill", "black")
5481      .attr("fill", (d) => (d.multiplier == 0 ? "#bbbbbb" : "#000000"))
5482      .attr("class", "team_box_pts")
5483      .text((d) => d.rp || 0);
5484  }
5485
5486  if (app.show_info_ts) {
5487    let td = app.team_data;
5488    let chips = Object.entries(td)
5489      .map((i) => [i[0], i[1].active_chip])
5490      .filter((j) => j[1]);
5491
5492    let chip_dict = {
5493      wildcard: "WC",
5494      "3xc": "TC",
5495      freehit: "FH",
5496      bboost: "BB",
5497    };
5498
5499    body
5500      .append("g")
5501      .selectAll()
5502      .data(chips)
5503      .enter()
5504      .append("text")
5505      .attr("text-anchor", "middle")
5506      .attr("alignment-baseline", "middle")
5507      .attr("dominant-baseline", "middle")
5508      .attr("x", (d) => x2("Chip") + x2.bandwidth() / 2)
5509      .attr("y", (d) => y(d[0]) + y.bandwidth() / 2 + 1)
5510      .attr("font-size", "4pt")
5511      .attr("fill", "white")
5512      .text((d) => chip_dict[d[1]]);
5513
5514    let gw_pts_text = (e) => {
5515      if (
5516        e.entry_history &&
5517        e.entry_history.event_transfers_cost &&
5518        e.entry_history.event_transfers_cost > 0
5519      ) {
5520        return (
5521          e.entry_history.points +
5522          " (-" +
5523          e.entry_history.event_transfers_cost +
5524          ")"
5525        );
5526      } else if (e.entry_history) {
5527        return e.entry_history.points;
5528      } else {
5529        return "-";
5530      }
5531    };
5532
5533    let pts_vals = Object.entries(td).map((i) => [i[0], gw_pts_text(i[1])]);
5534
5535    body
5536      .append("g")
5537      .selectAll()
5538      .data(pts_vals)
5539      .enter()
5540      .append("text")
5541      .attr("text-anchor", "middle")
5542      .attr("alignment-baseline", "middle")
5543      .attr("dominant-baseline", "middle")
5544      .attr("x", (d) => x2("Pts") + x2.bandwidth() / 2)
5545      .attr("y", (d) => y(d[0]) + y.bandwidth() / 2 + 1)
5546      .attr("font-size", "4pt")
5547      .attr("fill", "white")
5548      .text((d) => d[1]);
5549
5550    let rank_vals = Object.entries(td).map((i) => [
5551      i[0],
5552      _.get(i[1], "entry_history.overall_rank"),
5553    ]);
5554
5555    // formatted_rank
5556
5557    body
5558      .append("g")
5559      .selectAll()
5560      .data(rank_vals)
5561      .enter()
5562      .append("text")
5563      .attr("text-anchor", "middle")
5564      .attr("alignment-baseline", "middle")
5565      .attr("dominant-baseline", "middle")
5566      .attr("x", (d) => x2("OR") + x2.bandwidth() / 2)
5567      .attr("y", (d) => y(d[0]) + y.bandwidth() / 2 + 1)
5568      .attr("font-size", "4pt")
5569      .attr("fill", "white")
5570      .text((d) => formatnumber(d[1]));
5571  }
5572}
5573
5574function draw_point_origin_graph() {
5575  if (!app.is_ready) {
5576    return;
5577  }
5578
5579  const raw_width = 620;
5580  const raw_height = 620;
5581
5582  const margin = { top: 10, right: 10, bottom: 10, left: 10 },
5583    width = raw_width - margin.left - margin.right,
5584    height = raw_height - margin.top - margin.bottom;
5585
5586  let data = _.cloneDeep(app.user_picks_origin);
5587
5588  let gws = Object.keys(data.data);
5589
5590  jQuery("#point_origin_visual").empty();
5591
5592  const raw_svg = d3
5593    .select("#point_origin_visual")
5594    .append("svg")
5595    .attr(
5596      "viewBox",
5597      `0 0  ${width + margin.left + margin.right} ${
5598        height + margin.top + margin.bottom
5599      }`
5600    )
5601    .attr("class", "pull-center")
5602    .style("display", "block")
5603    .style("margin-bottom", "10px");
5604
5605  let svg = raw_svg
5606    .append("g")
5607    .attr("class", "mainbg")
5608    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
5609
5610  // let x = d3.scaleLinear().domain([0, data.stats.total]).range([0, width])
5611  // svg.append('g').attr('transform', 'translate(0,' + height + ')').call(d3.axisBottom(x).tickSize(0));
5612
5613  // section 1
5614  // total points earned
5615  let s1_top = 0;
5616  let s1_end = 0;
5617  let s1 = svg.append("g").attr("transform", "translate(0, 0)");
5618  if (true) {
5619    let local_data = data.stats.pts_main;
5620
5621    // title
5622    s1.append("text")
5623      .attr("text-anchor", "middle")
5624      .attr("alignment-baseline", "hanging")
5625      .attr("dominant-baseline", "hanging")
5626      .attr("x", width / 2)
5627      .attr("y", 0)
5628      .attr("font-size", "8pt")
5629      .attr("fill", "white")
5630      .text("Total Points Earned by Acquisition Type");
5631
5632    let categories = {
5633      Initial: "initial",
5634      Wildcard: "wildcard",
5635      Transfers: "transfer",
5636      "Free Hit": "freehit",
5637      //   "Assistant Manager": "manager",
5638    };
5639    let cat_keys = Object.keys(categories);
5640
5641    let all_values = [];
5642
5643    let box_value = 10;
5644    let row_count = 5;
5645    let col_start = 0;
5646
5647    // AM fix
5648
5649    cat_keys.forEach((c) => {
5650      let t_value = local_data[categories[c]];
5651      let col_count = Math.ceil(t_value / box_value / row_count);
5652
5653      entry = {
5654        cat: c,
5655        total_score: t_value,
5656        cat_tag: categories[c],
5657        cols: col_count,
5658        col_start: col_start,
5659        col_end: col_start + col_count,
5660      };
5661      col_start += col_count;
5662      let col_boxes = [];
5663      let score = t_value;
5664      _.range(entry.cols).forEach((col) => {
5665        col_values = [];
5666        while (col_values.length < row_count && score > 0) {
5667          let e_score = score > box_value ? box_value : score;
5668          col_values.push(e_score);
5669          score -= e_score;
5670        }
5671        col_boxes.push(col_values);
5672      });
5673      entry.col_boxes = col_boxes;
5674      all_values.push(entry);
5675    });
5676
5677    let xdomain = _.range(0, _.maxBy(all_values, "col_end").col_end + 1);
5678    if (app.next_gw != "39") {
5679      xdomain = _.range(0, 60);
5680    }
5681
5682    let x = d3.scaleBand().domain(xdomain).range([0, width]).paddingInner(0.15);
5683    let y = d3
5684      .scaleBand()
5685      .domain(_.range(row_count))
5686      .range([0, x.step() * row_count])
5687      .paddingInner(0.15);
5688
5689    let y_end = (s1_end = x.step() * row_count);
5690
5691    let group_holder = s1
5692      .append("g")
5693      .attr("transform", "translate(0, 20)")
5694      .selectAll()
5695      .data(all_values)
5696      .enter()
5697      .append("g")
5698      .attr("transform", (d) => `translate(${x(d.col_start)}, 0)`);
5699
5700    let box_holders = group_holder
5701      .selectAll()
5702      .data((d, g) =>
5703        d.col_boxes.map((val, inner_col) => {
5704          return { val: val, inner_col: inner_col, group: g };
5705        })
5706      )
5707      .enter();
5708
5709    type_cols = ["#d65544", "#48baff", "#93dea6", "#ffdc31", "purple"];
5710
5711    let single_node = box_holders
5712      .selectAll()
5713      .data((d) =>
5714        d.val.map((k, r) => {
5715          return { val: k, row: r, col: d.inner_col, group: d.group };
5716        })
5717      )
5718      .enter();
5719
5720    // gray box
5721    single_node
5722      .append("rect")
5723      .attr("x", (d) => x(d.col))
5724      .attr("y", (d) => y(d.row))
5725      .attr("width", x.bandwidth())
5726      .attr("height", y.bandwidth())
5727      .attr("fill", "#686868")
5728      .attr("stroke", "none");
5729
5730    // color box
5731    single_node
5732      .append("rect")
5733      .attr("x", (d) => x(d.col))
5734      .attr("y", (d) => y(d.row))
5735      .attr("width", (d) => (x.bandwidth() * d.val) / box_value)
5736      .attr("height", y.bandwidth())
5737      .attr("fill", (d) => type_cols[d.group])
5738      .attr("stroke", "none")
5739      .attr("data-val", (d) => d.val)
5740      .attr("data-col", (d) => d.col)
5741      .attr("data-row", (d) => d.row);
5742
5743    // legend box
5744    let single_width = 100;
5745
5746    let legend_box = s1
5747      .append("g")
5748      .attr(
5749        "transform",
5750        `translate(${(width - cat_keys.length * single_width) / 2}, ${
5751          y_end + 30
5752        })`
5753      );
5754
5755    let legend_entry = legend_box.selectAll().data(all_values).enter();
5756
5757    let single_entry = legend_entry
5758      .append("g")
5759      .attr("transform", (d, i) => `translate(${i * single_width}, 0)`);
5760
5761    single_entry
5762      .append("rect")
5763      .attr("x", 0)
5764      .attr("y", 0)
5765      .attr("width", 10)
5766      .attr("height", 10)
5767      .attr("fill", (d, i) => type_cols[i])
5768      .attr("stroke", "none");
5769
5770    single_entry
5771      .append("text")
5772      .attr("text-anchor", "start")
5773      .attr("alignment-baseline", "middle")
5774      .attr("dominant-baseline", "middle")
5775      .attr("x", 15)
5776      .attr("y", 6)
5777      .attr("font-size", "7pt")
5778      .attr("fill", "white")
5779      .text((d) => d.cat);
5780
5781    single_entry
5782      .append("text")
5783      .attr("text-anchor", "start")
5784      .attr("alignment-baseline", "middle")
5785      .attr("dominant-baseline", "middle")
5786      .attr("x", 15)
5787      .attr("y", 16)
5788      .attr("font-size", "7pt")
5789      .attr("fill", "white")
5790      .text((d) => d.total_score + " pts");
5791
5792    single_entry
5793      .append("text")
5794      .attr("text-anchor", "start")
5795      .attr("alignment-baseline", "middle")
5796      .attr("dominant-baseline", "middle")
5797      .attr("x", 15)
5798      .attr("y", 26)
5799      .attr("font-size", "7pt")
5800      .attr("fill", "white")
5801      .text((d) => _.round((d.total_score / data.stats.total) * 100, 0) + "%");
5802
5803    // svg.append('rect').attr('x', 0).attr('y', y_end + 70).attr('width', 100).attr('height', 100)
5804  }
5805
5806  let s2_top = Math.ceil(s1_end + 75);
5807  let s2 = svg.append("g").attr("transform", `translate(0, ${s2_top})`);
5808  if (true) {
5809    // work rect:
5810    // s2.append("rect")
5811    // .attr("x", 0)
5812    // .attr("y", 0)
5813    // .attr("width", width)
5814    // .attr("height", 390)
5815    // .attr("fill", "red")
5816    // .attr("fill-opacity", 0)
5817    // .attr("stroke", "white")
5818    // .attr("stroke-opacity", 0.1)
5819
5820    let age_groups = data.age_groups;
5821    let group_names = data.group_names;
5822    let total = data.nested_sum;
5823    let count = data.nested_count;
5824
5825    // title
5826    s2.append("text")
5827      .attr("text-anchor", "middle")
5828      .attr("alignment-baseline", "hanging")
5829      .attr("dominant-baseline", "hanging")
5830      .attr("x", width / 2)
5831      .attr("y", 0)
5832      .attr("font-size", "8pt")
5833      .attr("fill", "white")
5834      .text(
5835        "Total Points, Counts, Points per Pick by Decision Age and Acquisition Type"
5836      );
5837
5838    let graph_inner = s2.append("g").attr("transform", "translate(0, 35)");
5839
5840    let x = d3
5841      .scaleBand()
5842      .range([0, width])
5843      .domain(
5844        ["Age"].concat(group_names.map((i) => i[0])).concat(["Total", "Perc."])
5845      )
5846      .padding(0.05);
5847    graph_inner
5848      .append("g")
5849      .attr("transform", "translate(0,0)")
5850      .call(d3.axisTop(x).tickSize(0));
5851    let y = d3
5852      .scaleBand()
5853      .range([0, 340])
5854      .domain(age_groups.concat(["Total"]))
5855      .padding(0.1);
5856    // graph_inner.append('g').call(d3.axisLeft(y).tickSize(0));
5857    //.select(".domain").remove();
5858
5859    graph_inner.call((g) => g.selectAll(".domain").attr("opacity", 0));
5860
5861    let values_as_list = age_groups
5862      .map((age) =>
5863        group_names.map((gr) => {
5864          return {
5865            age: age,
5866            group: gr,
5867            group_text: gr[0],
5868            val: _.get(total, gr[1] + "." + age) || 0,
5869            total: _.get(total, gr[1] + "." + age) || "",
5870            count: _.get(count, gr[1] + "." + age) || "",
5871            ppp: _.round(
5872              (_.get(total, gr[1] + "." + age) || 0) /
5873                (_.get(count, gr[1] + "." + age) || 1),
5874              1
5875            ),
5876          };
5877        })
5878      )
5879      .flat();
5880    console.log(values_as_list);
5881
5882    let color_range = d3
5883      .scaleLinear()
5884      .range(["#ffffff", "#00fff8"])
5885      .domain([0, d3.max(values_as_list.map((i) => i.total))]);
5886
5887    // y labels
5888    y_labels = graph_inner
5889      .append("g")
5890      .selectAll()
5891      .data(age_groups.concat("Total"))
5892      .enter();
5893    y_labels
5894      .append("text")
5895      .attr("x", x("Age") + x.bandwidth() / 2)
5896      .attr("y", (d) => y(d) + y.bandwidth() / 2 + 1)
5897      .attr("text-anchor", "middle")
5898      .attr("alignment-baseline", "middle")
5899      .attr("dominant-baseline", "middle")
5900      .attr("font-size", "7pt")
5901      .attr("fill", "white")
5902      .text((d) => d);
5903
5904    // data cells
5905
5906    let cell = graph_inner.append("g").selectAll().data(values_as_list).enter();
5907
5908    cell
5909      .append("rect")
5910      .attr("x", (d) => x(d.group[0]))
5911      .attr("y", (d) => y(d.age))
5912      .attr("width", x.bandwidth())
5913      .attr("height", y.bandwidth())
5914      .attr("rx", "2px")
5915      .attr("ry", "2px")
5916      // .attr("fill", d => color_range(d.total))
5917      .attr("fill", "#686868")
5918      .attr("fill-opacity", 0.5);
5919
5920    cell
5921      .append("text")
5922      .attr("x", (d) => x(d.group[0]) + x.bandwidth() / 2)
5923      .attr("y", (d) => y(d.age) + (y.bandwidth() * 3.5) / 10)
5924      .attr("text-anchor", "middle")
5925      .attr("alignment-baseline", "middle")
5926      .attr("dominant-baseline", "middle")
5927      .attr("font-size", "7pt")
5928      .attr("fill", (d) => color_range(d.total)) // "#abf8f9")
5929      .text((d) => (d.total != "" ? d.total + " pts" : ""));
5930
5931    cell
5932      .append("text")
5933      .attr("x", (d) => x(d.group[0]) + x.bandwidth() / 2)
5934      .attr("y", (d) => y(d.age) + (y.bandwidth() * 8.5) / 10)
5935      .attr("text-anchor", "middle")
5936      .attr("alignment-baseline", "middle")
5937      .attr("dominant-baseline", "middle")
5938      .attr("font-size", "4pt")
5939      .attr("fill", "white")
5940      .text((d) =>
5941        d.count != "" ? d.count + " picks / " + d.ppp + " ppp" : ""
5942      );
5943
5944    let total_cells = graph_inner
5945      .append("g")
5946      .selectAll()
5947      .data(Object.entries(data.age_sums))
5948      .enter();
5949
5950    total_cells
5951      .append("text")
5952      .attr("x", x("Total") + x.bandwidth() / 2)
5953      .attr("y", (d) => y(d[0]) + (y.bandwidth() * 3.5) / 10)
5954      .attr("text-anchor", "middle")
5955      .attr("alignment-baseline", "middle")
5956      .attr("dominant-baseline", "middle")
5957      .attr("font-size", "7pt")
5958      .attr("fill", "white")
5959      .text((d) => d[1] + " pts");
5960
5961    total_cells
5962      .append("text")
5963      .attr("x", x("Total") + x.bandwidth() / 2)
5964      .attr("y", (d) => y(d[0]) + (y.bandwidth() * 8.5) / 10)
5965      .attr("text-anchor", "middle")
5966      .attr("alignment-baseline", "middle")
5967      .attr("dominant-baseline", "middle")
5968      .attr("font-size", "4pt")
5969      .attr("fill", "white")
5970      .text(
5971        (d) =>
5972          (data.age_cnt[d[0]] || 0) +
5973          " picks / " +
5974          _.round(d[1] / (data.age_cnt[d[0]] || 1), 1) +
5975          " ppp"
5976      );
5977
5978    let overall_sum = data.stats.total;
5979
5980    total_cells
5981      .append("text")
5982      .attr("x", x("Perc.") + x.bandwidth() / 2)
5983      .attr("y", (d) => y(d[0]) + y.bandwidth() / 2 + 1)
5984      .attr("text-anchor", "middle")
5985      .attr("alignment-baseline", "middle")
5986      .attr("dominant-baseline", "middle")
5987      .attr("font-size", "7pt")
5988      .attr("fill", "white")
5989      .text((d) => _.round((100 * d[1]) / overall_sum, 0) + "%");
5990
5991    // total for y
5992    let gr_dict = Object.fromEntries(data.group_names);
5993    let gr_sums = _(values_as_list)
5994      .groupBy("group_text")
5995      .map((val, key) => [
5996        key,
5997        _.sum(val.map((i) => parseInt(i.val))),
5998        data.grp_cnt[gr_dict[key]] || 0,
5999      ])
6000      .value();
6001
6002    let total_groups = graph_inner
6003      .append("g")
6004      .selectAll()
6005      .data(gr_sums)
6006      .enter();
6007
6008    total_groups
6009      .append("text")
6010      .attr("x", (d) => x(d[0]) + x.bandwidth() / 2)
6011      .attr("y", (d) => y("Total") + (y.bandwidth() * 3.5) / 10 + 1)
6012      .attr("text-anchor", "middle")
6013      .attr("alignment-baseline", "middle")
6014      .attr("dominant-baseline", "middle")
6015      .attr("font-size", "7pt")
6016      .attr("fill", "white")
6017      .text((d) => d[1] + " pts");
6018
6019    total_groups
6020      .append("text")
6021      .attr("x", (d) => x(d[0]) + x.bandwidth() / 2)
6022      .attr("y", (d) => y("Total") + (y.bandwidth() * 8.5) / 10 + 1)
6023      .attr("text-anchor", "middle")
6024      .attr("alignment-baseline", "middle")
6025      .attr("dominant-baseline", "middle")
6026      .attr("font-size", "4pt")
6027      .attr("fill", "white")
6028      .text(
6029        (d) => d[2] + " picks / " + _.round(d[1] / (d[2] || 1), 1) + " ppp"
6030      );
6031
6032    graph_inner
6033      .append("text")
6034      .attr("x", (d) => x("Total") + x.bandwidth() / 2)
6035      .attr("y", (d) => y("Total") + y.bandwidth() / 2 + 1)
6036      .attr("text-anchor", "middle")
6037      .attr("alignment-baseline", "middle")
6038      .attr("dominant-baseline", "middle")
6039      .attr("font-size", "7pt")
6040      .attr("fill", "white")
6041      .text(overall_sum + " pts");
6042  }
6043
6044  let s3_top = Math.ceil(s2_top + 390);
6045  let s3 = svg.append("g").attr("transform", `translate(0, ${s3_top})`);
6046  if (true) {
6047    s3.append("text")
6048      .attr("text-anchor", "middle")
6049      .attr("alignment-baseline", "hanging")
6050      .attr("dominant-baseline", "hanging")
6051      .attr("x", width / 2)
6052      .attr("y", 0)
6053      .attr("font-size", "8pt")
6054      .attr("fill", "white")
6055      .text("Team Composition");
6056
6057    let gws = _.range(1, 39);
6058
6059    let x = d3.scaleBand().range([30, width]).domain(gws).padding(0.1);
6060    let y = d3
6061      .scaleBand()
6062      .range([0, 150])
6063      .domain(_.range(15))
6064      .paddingInner(0.1);
6065
6066    let gw_chips = _(data.data)
6067      .mapValues((val, key) => val.active_chip)
6068      .value();
6069    let all_players = data.all_players;
6070    let nodes = [];
6071    let links = [];
6072    gws.forEach((gw) => {
6073      let gw_picks = all_players.filter((i) => i.gw == gw);
6074      let gw_acq = _.uniq(gw_picks.map((i) => i.origin_text));
6075      let picked_groups = data.group_names.filter((i) => gw_acq.includes(i[1]));
6076      let cnt = 0;
6077      let lineup_cnt = 0;
6078      picked_groups.forEach((group) => {
6079        let picks = gw_picks.filter((i) => i.origin_text == group[1]);
6080        let val = picks.length;
6081        let lineup_val = picks.filter((i) => i.multiplier > 0).length;
6082        nodes.push({
6083          type: "regular",
6084          x0: x(gw),
6085          x1: x(gw) + x.bandwidth(),
6086          y0: y(cnt),
6087          y1: y(cnt + val - 1) + y.bandwidth(),
6088          group: group,
6089          val: val,
6090          pts: _.sum(picks.map((i) => i.eff_points)),
6091          lineup: lineup_val,
6092          bench: picks.filter((i) => i.multiplier == 0).length,
6093          ly0: y(lineup_cnt),
6094          ly1: y(lineup_cnt + lineup_val - 1) + y.bandwidth(),
6095        });
6096        cnt += val;
6097        lineup_cnt += lineup_val;
6098      });
6099      if (lineup_cnt < 15) {
6100        nodes.push({
6101          type: "bench",
6102          x0: x(gw),
6103          x1: x(gw) + x.bandwidth(),
6104          group: ["", "bench", ""],
6105          bench: picks.filter((i) => i.multiplier == 0).length,
6106          lineup: 15 - lineup_cnt,
6107          ly0: y(lineup_cnt),
6108          ly1: y(14) + y.bandwidth(),
6109        });
6110      }
6111    });
6112
6113    let s3_p1 = s3.append("g").attr("transform", "translate(0,20)");
6114
6115    type_cols = {
6116      bench: "gray",
6117      initial: "#d65544",
6118      wildcard: "#48baff",
6119      wc1: "#48baff",
6120      wc2: "#48baff",
6121      // 'wcb': '#48baff',
6122      transfer: "#93dea6",
6123      freehit: "#ffdc31",
6124      fh1: "#ffdc31",
6125      fh2: "#ffdc31",
6126    };
6127    gr_text_color = {
6128      bench: "white",
6129      initial: "white",
6130      wildcard: "black",
6131      wc1: "black",
6132      wc2: "black",
6133      transfer: "black",
6134      fh1: "black",
6135      fh2: "black",
6136      freehit: "black",
6137    };
6138
6139    {
6140      // points
6141      s3_p1
6142        .append("text")
6143        .attr("text-anchor", "start")
6144        .attr("alignment-baseline", "middle")
6145        .attr("dominant-baseline", "middle")
6146        .attr("x", 0)
6147        .attr("y", 0)
6148        .attr("font-size", "6pt")
6149        .attr("fill", "#7fd3d9")
6150        .text("Points");
6151
6152      s3_p1
6153        .selectAll()
6154        .data(gws)
6155        .enter()
6156        .append("text")
6157        .attr("text-anchor", "middle")
6158        .attr("alignment-baseline", "middle")
6159        .attr("dominant-baseline", "middle")
6160        .attr("x", (d) => x(d) + x.bandwidth() / 2)
6161        .attr("y", 0)
6162        .attr("font-size", "4pt")
6163        .attr("fill", "white")
6164        .text((d) => d);
6165
6166      let main_colors = {
6167        IN: "#d65544",
6168        WC: "#48baff",
6169        TR: "#93dea6",
6170        FH: "#ffdc31",
6171      };
6172      let ymain = d3
6173        .scaleBand()
6174        .range([0, 48])
6175        .domain(data.main_groups.map((i) => i[1]))
6176        .paddingInner(0.1);
6177
6178      let point_holder = s3_p1
6179        .append("g")
6180        .attr("transform", "translate(0, 10)");
6181
6182      let leg_box = point_holder
6183        .selectAll()
6184        .data(data.main_groups)
6185        .enter()
6186        .append("g");
6187
6188      leg_box
6189        .append("rect")
6190        .attr("x", 15)
6191        .attr("y", (d, i) => ymain(d[1]))
6192        .attr("width", 10)
6193        .attr("height", ymain.bandwidth())
6194        .attr("fill", (d) => main_colors[d[2]])
6195        .attr("stroke", "none");
6196
6197      leg_box
6198        .append("text")
6199        .attr("text-anchor", "middle")
6200        .attr("alignment-baseline", "middle")
6201        .attr("dominant-baseline", "middle")
6202        .attr("x", 20)
6203        .attr("y", (d, i) => ymain(d[1]) + ymain.bandwidth() / 2)
6204        .attr("font-size", "5pt")
6205        .attr("fill", (d) => gr_text_color[d[1]])
6206        .text((d) => d[2]);
6207
6208      let type_vals = _(data.all_players)
6209        .groupBy("origin")
6210        .map((val, key) =>
6211          _(val)
6212            .groupBy("gw")
6213            .map((ival, ikey) => {
6214              return {
6215                cnt: ival.filter((i) => i.multiplier > 0).length,
6216                val: _.sum(ival.map((i) => i.eff_points)),
6217                gw: ikey,
6218                group: key,
6219              };
6220            })
6221            .value()
6222        )
6223        .value()
6224        .flat();
6225
6226      let type_box = point_holder
6227        .append("g")
6228        .selectAll()
6229        .data(type_vals)
6230        .enter();
6231
6232      type_box
6233        .append("text")
6234        .attr("text-anchor", "middle")
6235        .attr("alignment-baseline", "middle")
6236        .attr("dominant-baseline", "middle")
6237        .attr("x", (d) => x(d.gw) + x.bandwidth() / 2)
6238        .attr("y", (d) => ymain(d.group) + ymain.bandwidth() / 2)
6239        .attr("font-size", "5pt")
6240        .attr("fill", (d) => type_cols[d.group])
6241        .text((d) => d.val);
6242    }
6243
6244    // data part
6245
6246    let s3_p2 = s3.append("g").attr("transform", "translate(0,90)");
6247
6248    {
6249      // points
6250      s3_p2
6251        .append("text")
6252        .attr("text-anchor", "start")
6253        .attr("alignment-baseline", "middle")
6254        .attr("dominant-baseline", "middle")
6255        .attr("x", 0)
6256        .attr("y", 85)
6257        .attr("font-size", "6pt")
6258        .attr("fill", "#7fd3d9")
6259        .text("Squad");
6260
6261      s3_p2
6262        .selectAll()
6263        .data(gws)
6264        .enter()
6265        .append("text")
6266        .attr("text-anchor", "middle")
6267        .attr("alignment-baseline", "middle")
6268        .attr("dominant-baseline", "middle")
6269        .attr("x", (d) => x(d) + x.bandwidth() / 2)
6270        .attr("y", 0)
6271        .attr("font-size", "4pt")
6272        .attr("fill", "white")
6273        .text((d) => d);
6274
6275      let boxes = s3_p2
6276        .append("g")
6277        .attr("transform", "translate(0,10)")
6278        .selectAll()
6279        .data(nodes.filter((i) => i.type == "regular"))
6280        .enter();
6281
6282      boxes
6283        .append("rect")
6284        .attr("x", (d) => d.x0)
6285        .attr("y", (d) => d.y0)
6286        .attr("width", (d) => d.x1 - d.x0)
6287        .attr("height", (d) => d.y1 - d.y0)
6288        .attr("fill", (d) => type_cols[d.group[1]])
6289        // .attr("fill-opacity", 0.1)
6290        .attr("stroke", "none");
6291
6292      // boxes.append("text")
6293      //     .attr("text-anchor", "middle")
6294      //     .attr("alignment-baseline", "middle")
6295      //     .attr("dominant-baseline", "middle")
6296      //     .attr("x", d => (d.x0 + d.x1)/2)
6297      //     .attr("y", d => (d.y0+d.y1)/2 - 6)
6298      //     .attr("font-size", "5pt")
6299      //     .attr("fill", d => gr_text_color[d.group[1]])
6300      //     .text(d => d.pts + " P")
6301
6302      boxes
6303        .append("text")
6304        .attr("text-anchor", "middle")
6305        .attr("alignment-baseline", "middle")
6306        .attr("dominant-baseline", "middle")
6307        .attr("x", (d) => (d.x0 + d.x1) / 2)
6308        .attr("y", (d) => (d.y0 + d.y1) / 2)
6309        .attr("font-size", "5pt")
6310        .attr("fill", (d) => gr_text_color[d.group[1]])
6311        .text((d) => d.val);
6312
6313      // boxes.append("text")
6314      //     .attr("text-anchor", "middle")
6315      //     .attr("alignment-baseline", "middle")
6316      //     .attr("dominant-baseline", "middle")
6317      //     .attr("x", d => (d.x0 + d.x1)/2)
6318      //     .attr("y", d => (d.y0+d.y1)/2 + 0)
6319      //     .attr("font-size", "5pt")
6320      //     .attr("fill", d => gr_text_color[d.group[1]])
6321      //     .text(d => 'L: ' + d.lineup)
6322
6323      // boxes.append("text")
6324      //     .attr("text-anchor", "middle")
6325      //     .attr("alignment-baseline", "middle")
6326      //     .attr("dominant-baseline", "middle")
6327      //     .attr("x", d => (d.x0 + d.x1)/2)
6328      //     .attr("y", d => (d.y0+d.y1)/2 + 6)
6329      //     .attr("font-size", "5pt")
6330      //     .attr("fill", d => gr_text_color[d.group[1]])
6331      //     .text(d => 'B: ' + d.bench)
6332    }
6333
6334    let s3_p3 = s3.append("g").attr("transform", "translate(0,260)");
6335
6336    {
6337      //
6338      s3_p3
6339        .append("text")
6340        .attr("text-anchor", "start")
6341        .attr("alignment-baseline", "middle")
6342        .attr("dominant-baseline", "middle")
6343        .attr("x", 0)
6344        .attr("y", 85)
6345        .attr("font-size", "6pt")
6346        .attr("fill", "#7fd3d9")
6347        .text("Lineup");
6348
6349      s3_p3
6350        .selectAll()
6351        .data(gws)
6352        .enter()
6353        .append("text")
6354        .attr("text-anchor", "middle")
6355        .attr("alignment-baseline", "middle")
6356        .attr("dominant-baseline", "middle")
6357        .attr("x", (d) => x(d) + x.bandwidth() / 2)
6358        .attr("y", 0)
6359        .attr("font-size", "4pt")
6360        .attr("fill", "white")
6361        .text((d) => d);
6362
6363      let lineup_boxes = s3_p3
6364        .append("g")
6365        .attr("transform", "translate(0,10)")
6366        .selectAll()
6367        .data(nodes.filter((i) => i.lineup != 0))
6368        .enter();
6369
6370      lineup_boxes
6371        .append("rect")
6372        .attr("x", (d) => d.x0)
6373        .attr("y", (d) => d.ly0)
6374        .attr("width", (d) => d.x1 - d.x0)
6375        .attr("height", (d) => d.ly1 - d.ly0)
6376        .attr("fill", (d) => type_cols[d.group[1]])
6377        .attr("stroke", "none");
6378
6379      lineup_boxes
6380        .append("text")
6381        .attr("text-anchor", "middle")
6382        .attr("alignment-baseline", "middle")
6383        .attr("dominant-baseline", "middle")
6384        .attr("x", (d) => (d.x0 + d.x1) / 2)
6385        .attr("y", (d) => (d.ly0 + d.ly1) / 2)
6386        .attr("font-size", "5pt")
6387        .attr("fill", (d) => gr_text_color[d.group[1]])
6388        .text((d) => d.lineup);
6389    }
6390  }
6391
6392  // final height update
6393
6394  let n_height = Math.ceil(s3_top + 420);
6395  raw_svg.attr(
6396    "viewBox",
6397    `0 0  ${width + margin.left + margin.right} ${
6398      n_height + margin.top + margin.bottom
6399    }`
6400  );
6401}
6402
6403var unitlist = ["", "K", "M", "G"];
6404function formatnumber(number) {
6405  let sign = Math.sign(number);
6406  let unit = 0;
6407
6408  while (Math.abs(number) >= 1000) {
6409    unit = unit + 1;
6410    number = Math.floor(Math.abs(number) / 100) / 10;
6411  }
6412  return sign * Math.abs(number) + unitlist[unit];
6413}
6414
6415async function get_points() {
6416  return read_cached_rp(app.season)
6417    .then((data) => {
6418      app.points_data = Object.freeze(data);
6419    })
6420    .catch((e) => {
6421      console.log(e);
6422      return getSeasonRPData(parseInt(gw)).then((data) => {
6423        app.points_data = Object.freeze(data);
6424      });
6425    });
6426
6427  // return $.ajax({
6428  //     type: "GET",
6429  //     url: `data/${season}/points.json`,
6430  //     async: true,
6431  //     dataType: "json",
6432  //     success: (data) => {
6433  //         app.points_data = data;
6434  //     },
6435  //     error: (xhr, status, error) => {
6436  //         console.log(error);
6437  //         console.error(xhr, status, error);
6438  //     }
6439  // });
6440}
6441
6442async function get_eo() {
6443  return $.ajax({
6444    type: "GET",
6445    url: `data/${season}/eo.json`,
6446    async: true,
6447    dataType: "json",
6448    success: (data) => {
6449      // app.eo_data = Object.freeze(data);
6450      // data = _.mapValues(data, v => _.pickBy(v, (v2,key) => ['Overall', 'Prime'].includes(key)));
6451      app.eo_data = Object.freeze(data);
6452      let target_key = Math.max(...Object.keys(data).map((i) => parseInt(i)));
6453      app.sample_options = Object.keys(data[target_key]);
6454      // default to Solio Analytics if available, otherwise first non-Overall tier
6455      let solio_idx = app.sample_options.indexOf("Solio Analytics");
6456      if (solio_idx >= 0) {
6457        app.sample_selection = solio_idx;
6458      } else if (app.sample_options.length > 1) {
6459        app.sample_selection = 1;
6460      } else {
6461        app.sample_selection = 0;
6462      }
6463    },
6464    error: (xhr, status, error) => {
6465      console.log(error);
6466      console.error(xhr, status, error);
6467    },
6468  });
6469}
6470
6471async function get_fixture_data() {
6472  return $.ajax({
6473    type: "GET",
6474    url: `data/${season}/fixture.json`,
6475    async: true,
6476    dataType: "json",
6477    success: (data) => {
6478      app.fixture_data = Object.freeze(data);
6479    },
6480    error: (xhr, status, error) => {
6481      console.log(error);
6482      console.error(xhr, status, error);
6483    },
6484  });
6485}
6486
6487async function get_538_data() {
6488  return $.ajax({
6489    type: "GET",
6490    url: `data/${season}/fivethirtyeight_spi.csv`,
6491    async: true,
6492    success: (data) => {
6493      let tablevals = data.split("\n").map((i) => i.split(","));
6494      let keys = tablevals[0];
6495      let values = tablevals.slice(1);
6496      let final_data = values.map((i) => _.zipObject(keys, i));
6497      app.fdr_data = Object.freeze(
6498        final_data.filter((i) => i.league == "Barclays Premier League")
6499      );
6500    },
6501    error: (xhr, status, error) => {
6502      console.log(error);
6503      console.error(xhr, status, error);
6504    },
6505  });
6506}
6507
6508async function fetch_main_data() {
6509  return read_cached_static(app.season)
6510    .then((data) => {
6511      app.el_data = Object.freeze(data["elements"]);
6512    })
6513    .catch((e) => {
6514      console.log(e);
6515      return get_fpl_main_data().then((data) => {
6516        app.el_data = Object.freeze(data["elements"]);
6517      });
6518    });
6519}
6520
6521async function fetch_xp_data() {
6522  return read_cached_xp(app.season).then((data) => {
6523    let xp_data = jQuery.csv.toObjects(data);
6524    xp_data = xp_data.filter((i) => parseFloat(i.price) < 20); // && parseFloat(i.xp) + parseFloat(i.rp) != 0)
6525    app.xp_data = Object.freeze(xp_data);
6526  });
6527}
6528
6529let name_dict = {
6530  "Alexander-Arnold": "TAA",
6531  "Ward-Prowse": "JWP",
6532  "Calvert-Lewin": "DCL",
6533};
6534
6535let long_name_str = (n) => {
6536  return name_dict[n] || (n.length > 10 ? n.slice(0, 8) + "..." : n);
6537};
6538
6539let detailed_point_cache = [];
6540
6541$(document).ready(() => {
6542  Promise.all([
6543    get_points(),
6544    get_eo(),
6545    get_fixture_data(),
6546    // get_538_data(),
6547    fetch_main_data(),
6548    fetch_xp_data(),
6549  ])
6550    .then((values) => {
6551      Vue.$cookies.config("120d");
6552
6553      app.$nextTick(() => {
6554        let cached_team = Vue.$cookies.get("team_id");
6555        if (cached_team !== null) {
6556          app.team_id = parseInt(cached_team);
6557          app.$nextTick(() => {
6558            app.fetch_team_picks();
6559          });
6560        }
6561      });
6562    })
6563    .catch((error) => {
6564      console.error("An error has occured: " + JSON.stringify(error));
6565    });
6566});

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.