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