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.