1// Helper function to get theme-aware colors 2function getThemeColors() { 3 const computedStyle = getComputedStyle(document.documentElement); 4 const colors = { 5 svgBg: computedStyle.getPropertyValue("--svg-bg").trim(), 6 svgText: computedStyle.getPropertyValue("--svg-text").trim(), 7 svgStroke: computedStyle.getPropertyValue("--svg-stroke").trim(), 8 chartGoal: computedStyle.getPropertyValue("--chart-goal").trim(), 9 chartAssist: computedStyle.getPropertyValue("--chart-assist").trim(), 10 chartCs: computedStyle.getPropertyValue("--chart-cs").trim(), 11 }; 12 console.log("Theme colors:", colors); // Debug log 13 return colors; 14} 15 16var app = new Vue({ 17 el: "#app", 18 data: { 19 first_draw: true, 20 player_count: 1, 21 players: [ 22 { 23 oid: 0, 24 name: "Player 1", 25 position: 3, 26 play: 100, 27 goal: 40, 28 assist: 28, 29 cs: 39, 30 }, 31 ], 32 players_cached: [], 33 // player_name: '', 34 // player_position: 3, 35 // player_play: 100, 36 // player_goal: 40, 37 // player_assist: 28, 38 // player_cs: 39, 39 colors: [], // Will be set dynamically from CSS variables 40 point_rates: { 41 1: { goal: 6, assist: 3, cs: 4, "2gc": -1 }, 42 2: { goal: 6, assist: 3, cs: 4, "2gc": -1 }, 43 3: { goal: 5, assist: 3, cs: 1, "2gc": 0 }, 44 4: { goal: 4, assist: 3, cs: 0, "2gc": 0 }, 45 }, 46 expected_points: [0], 47 goal_rate: [0], 48 assist_rate: [0], 49 gc_rate: [0], 50 goal_probs: [[]], 51 assist_probs: [[]], 52 gc_probs: [[]], 53 goal_points: [0], 54 assist_points: [0], 55 cs_points: [0], 56 gc_points: [0], 57 csgc_points: [0], // Clean sheet + GC 58 active_rates: [{}], 59 point_probs: [{}], 60 graph_updates: {}, 61 printed_name: {}, 62 point_combinations: [[]], 63 graph_data: [], 64 player_colors: d3.scaleOrdinal(d3.schemeTableau10), 65 counter: 2, 66 comparison: [], 67 show_rates: false, 68 // bet_fraction: undefined, 69 // bet_decimal: undefined, 70 // bet_american: undefined, 71 // bookmaker_margin: 7, 72 // bet_implied: undefined 73 }, 74 computed: {}, 75 mounted() { 76 // Initialize colors from CSS variables 77 this.updateColorsFromCSS(); 78 79 // Listen for theme changes 80 document.addEventListener("themeChanged", () => { 81 this.updateColorsFromCSS(); 82 // Clear existing charts and redraw if they exist 83 if (this.players_cached.length > 0) { 84 // Clear all chart containers 85 d3.select("#ev_graph").selectAll("*").remove(); 86 d3.select("#goal_prob_graph").selectAll("*").remove(); 87 d3.select("#assist_prob_graph").selectAll("*").remove(); 88 d3.select("#cs_prob_graph").selectAll("*").remove(); 89 d3.select("#gc_prob_graph").selectAll("*").remove(); 90 91 // Reset first_draw flag to ensure fresh charts 92 this.first_draw = true; 93 this.calculate(); 94 } 95 }); 96 }, 97 methods: { 98 catch_debug() { 99 debugger; 100 return 1; 101 }, 102 updateColorsFromCSS() { 103 const themeColors = getThemeColors(); 104 this.colors = [ 105 themeColors.chartGoal, 106 themeColors.chartAssist, 107 themeColors.chartCs, 108 ]; 109 }, 110 calculate() { 111 // Update colors in case theme changed 112 this.updateColorsFromCSS(); 113 114 // clear existing data 115 this.printed_name = {}; 116 this.goal_rate = {}; 117 this.assist_rate = {}; 118 this.gc_rate = {}; 119 this.goal_probs = {}; 120 this.assist_probs = {}; 121 this.gc_probs = {}; 122 this.active_rates = {}; 123 this.goal_points = {}; 124 this.assist_points = {}; 125 this.cs_points = {}; 126 this.gc_points = {}; 127 this.csgc_points = {}; 128 this.expected_points = {}; 129 this.point_combinations = []; 130 this.graph_data = []; 131 132 this.players.forEach((p, i) => { 133 this.calculate_single(i); 134 }); 135 this.plot_graphs(); 136 137 this.comparison = []; 138 if (this.player_count == 2) { 139 this.compare_two(); 140 } 141 }, 142 calculate_single(idx) { 143 let player = this.players[idx]; 144 145 if (player.goal >= 100) { 146 player.goal = 99; 147 } 148 if (player.goal < 0) { 149 player.goal = 0; 150 } 151 152 if (player.assist >= 100) { 153 player.assist = 99; 154 } 155 if (player.assist < 0) { 156 player.assist = 0; 157 } 158 159 if (player.cs > 100) { 160 player.cs = 100; 161 } 162 if (player.cs <= 0) { 163 player.cs = 1; 164 } 165 166 let p_n = (this.printed_name[idx] = player.name); 167 168 // then 169 let g_r = (this.goal_rate[idx] = this.percentage_to_lambda( 170 player.goal / 100 171 )); 172 let a_r = (this.assist_rate[idx] = this.percentage_to_lambda( 173 player.assist / 100 174 )); 175 let gc_r = (this.gc_rate[idx] = this.percentage_to_lambda( 176 (100 - player.cs) / 100 177 )); 178 179 let g_p = (this.goal_probs[idx] = this.get_poisson_probs(g_r)); 180 let a_p = (this.assist_probs[idx] = this.get_poisson_probs(a_r)); 181 let gc_p = (this.gc_probs[idx] = this.get_poisson_probs(gc_r)); 182 183 let point_rates = this.point_rates[player.position]; 184 this.active_rates[idx] = point_rates; 185 186 let goal_pts = (this.goal_points[idx] = g_r * point_rates["goal"]); 187 let assist_pts = (this.assist_points[idx] = a_r * point_rates["assist"]); 188 let cs_pts = (this.cs_points[idx] = gc_p[0] * point_rates["cs"]); 189 let gc_pts = (this.gc_points[idx] = gc_p 190 .map((v, i) => point_rates["2gc"] * v * Math.floor(i / 2)) 191 .reduce((a, b) => a + b, 0)); 192 let csgc_pts = (this.csgc_points[idx] = cs_pts + gc_pts); 193
194 let ep = (this.expected_points[idx] = goal_pts + assist_pts + csgc_pts); 195 196 let goal_data = _.range(0, 11).map((g) => { 197 return { 198 count: g, 199 points: point_rates["goal"] * g, 200 probability: g_p[g] * 100, 201 }; 202 }); 203 let assist_data = _.range(0, 11).map((g) => { 204 return { 205 count: g, 206 points: point_rates["assist"] * g, 207 probability: a_p[g] * 100, 208 }; 209 }); 210 let cs_data = [ 211 { count: "Yes", points: point_rates["cs"], probability: gc_p[0] * 100 }, 212 { count: "No", points: 0, probability: (1 - gc_p[0]) * 100 }, 213 ]; 214 let gc_data = _.range(0, 11).map((g) => { 215 return { 216 count: g, 217 points: point_rates["2gc"] * Math.floor(g / 2), 218 probability: gc_p[g] * 100, 219 }; 220 }); 221 222 this.graph_data[idx] = { 223 goal_data, 224 assist_data, 225 cs_data, 226 gc_data, 227 ep, 228 p_n, 229 }; 230 231 // if (this.first_draw) { 232 // this.graph_updates['goal'] = draw_generic({selector: "#goal_prob_graph", title: "Goal Probability vs Count" , x_title: "Goals Scored", y_title: "Probability %", data: goal_data, color: this.colors[0]}) 233 // this.graph_updates['assist'] = draw_generic({selector: "#assist_prob_graph", title: "Assist Probability vs Count", x_title: "Assists Made", y_title: "Probability %", data: assist_data, color: this.colors[1]}) 234 // this.graph_updates['cs'] = draw_generic({selector: "#cs_prob_graph", title: "Clean Sheet Probability", x_title: "Clean Sheet", y_title: "Probability %", data: cs_data, color: this.colors[2]}) 235 // this.graph_updates['gc'] = draw_generic({selector: "#gc_prob_graph", title: "Goals Conceded Probability vs Count", x_title: "Goals Conceded", y_title: "Probability %", data: gc_data, color: this.colors[2]}) 236 // } 237 // else { 238 // this.graph_updates['goal'](goal_data, p_n) 239 // this.graph_updates['assist'](assist_data, p_n) 240 // this.graph_updates['cs'](cs_data, p_n) 241 // this.graph_updates['gc'](gc_data, p_n) 242 // } 243 244 let all_combinations = {}; 245 for (let g of goal_data) { 246 for (let a of assist_data) { 247 for (let gc of gc_data) { 248 let entry = { 249 probability: 250 (g.probability / 100) * 251 (a.probability / 100) * 252 (gc.probability / 100), 253 points: 254 g.points + 255 a.points + 256 gc.points + 257 (gc.count == 0 ? cs_data[0].points : cs_data[1].points), 258 // 'values': {'goal': g.points, 'assist': a.points, 'gc': gc.points, 'cs': (gc.count == 0 ? cs_data[0].points : cs_data[1].points)} 259 }; 260 if (entry.points in all_combinations) { 261 all_combinations[entry.points].probability += entry.probability; 262 } else { 263 all_combinations[entry.points] = entry; 264 } 265 } 266 } 267 } 268 all_combinations = _.filter( 269 all_combinations, 270 (o) => o.probability > 1e-4 271 ); 272 all_combinations.forEach((e) => { 273 e.probability = e.probability * 100; 274 }); 275 276 // if (this.first_draw) { 277 // this.graph_updates['total'] = draw_ev_graph(all_combinations, ep, p_n) 278 // } 279 // else { 280 // this.graph_updates['total'](all_combinations, ep, p_n) 281 // } 282 283 this.point_combinations[idx] = all_combinations; 284 285 this.players_cached = _.cloneDeep(this.players); 286 287 this.$forceUpdate(); 288 }, 289 plot_graphs() { 290 let idx = 0; 291 292 let { goal_data, assist_data, cs_data, gc_data, ep, p_n } = 293 this.graph_data[idx]; 294 295 if (this.first_draw) { 296 this.graph_updates["goal"] = draw_generic({ 297 selector: "#goal_prob_graph", 298 title: "Goal Probability vs Count", 299 x_title: "Goals Scored", 300 y_title: "Probability %", 301 data: goal_data, 302 color: this.colors[0], 303 }); 304 this.graph_updates["assist"] = draw_generic({ 305 selector: "#assist_prob_graph", 306 title: "Assist Probability vs Count", 307 x_title: "Assists Made", 308 y_title: "Probability %", 309 data: assist_data, 310 color: this.colors[1], 311 }); 312 this.graph_updates["cs"] = draw_generic({ 313 selector: "#cs_prob_graph", 314 title: "Clean Sheet Probability", 315 x_title: "Clean Sheet", 316 y_title: "Probability %", 317 data: cs_data, 318 color: this.colors[2], 319 }); 320 this.graph_updates["gc"] = draw_generic({ 321 selector: "#gc_prob_graph", 322 title: "Goals Conceded Probability vs Count", 323 x_title: "Goals Conceded", 324 y_title: "Probability %", 325 data: gc_data, 326 color: this.colors[2], 327 }); 328 } else { 329 this.graph_updates["goal"](goal_data, p_n); 330 this.graph_updates["assist"](assist_data, p_n); 331 this.graph_updates["cs"](cs_data, p_n); 332 this.graph_updates["gc"](gc_data, p_n); 333 } 334 335 let all_combinations = this.point_combinations; 336 let ep_values = this.graph_data.map((i) => i.ep); 337 let player_names = this.graph_data.map((i) => i.p_n); 338 339 if (this.first_draw) { 340 this.graph_updates["total"] = draw_ev_graph( 341 all_combinations, 342 ep_values, 343 player_names, 344 this.player_colors 345 ); 346 } else { 347 this.graph_updates["total"](all_combinations, ep_values, player_names); 348 } 349 350 this.first_draw = false;
351 }, 352 percentage_to_lambda(p) { 353 // converts P(k>0) info to Poisson lambda 354 return -Math.log(1 - p); 355 }, 356 lambda_to_percentage(lambda) { 357 // converts P(k>0) info to Poisson lambda 358 return Math.exp(-lambda); 359 }, 360 get_poisson_probs(lambda, k_max = 10) { 361 // converts Poisson lambda into probabilities for each k 362 let rates = []; 363 let current_rate = Math.exp(-lambda); 364 rates.push(current_rate); 365 for (let k = 1; k <= k_max; k++) { 366 current_rate = (current_rate * lambda) / k; 367 rates.push(current_rate); 368 } 369 return rates; 370 }, 371 add_player() { 372 this.players.push({ 373 oid: this.counter, 374 name: "Player " + this.counter, 375 position: 3, 376 play: 100, 377 goal: 40, 378 assist: 28, 379 cs: 39, 380 }); 381 this.player_count += 1; 382 this.counter += 1; 383 this.$forceUpdate(); 384 }, 385 remove_player(i) { 386 this.players = this.players.filter((j) => j.oid != i.oid); 387 this.player_count = this.player_count - 1; 388 this.$forceUpdate(); 389 }, 390 compare_two() { 391 let p1 = this.point_combinations[0]; 392 let p2 = this.point_combinations[1]; 393 394 let p1_better = 0; 395 let p2_better = 0; 396 let players_equal = 0; 397 398 p1.forEach((c1) => { 399 // p1 better 400 let c2 = p2.filter((i) => i.points < c1.points); 401 p1_better += c2 402 .map((i) => ((i.probability / 100) * c1.probability) / 100) 403 .reduce((a, b) => a + b, 0); 404 // p2 better 405 c2 = p2.filter((i) => i.points > c1.points); 406 p2_better += c2 407 .map((i) => ((i.probability / 100) * c1.probability) / 100) 408 .reduce((a, b) => a + b, 0); 409 // players are equal 410 c2 = p2.filter((i) => i.points == c1.points); 411 players_equal += c2 412 .map((i) => ((i.probability / 100) * c1.probability) / 100) 413 .reduce((a, b) => a + b, 0); 414 }); 415 416 this.comparison = {}; 417 this.comparison = { p1_better, p2_better, players_equal }; 418 419 this.$forceUpdate(); 420 }, 421 toggle_rate() { 422 this.show_rates = !this.show_rates; 423 this.$nextTick(() => { 424 let goal_values = this.players.map((i) => 425 this.percentage_to_lambda(i.goal / 100) 426 ); 427 jQuery(".goal_rates").each((i, v) => { 428 v.value = parseFloat(goal_values[i].toFixed(4)); 429 }); 430 let assist_values = this.players.map((i) => 431 this.percentage_to_lambda(i.assist / 100) 432 ); 433 jQuery(".assist_rates").each((i, v) => { 434 v.value = parseFloat(assist_values[i].toFixed(4)); 435 }); 436 let gc_values = this.players.map((i) => 437 this.percentage_to_lambda((100 - i.cs) / 100) 438 ); 439 jQuery(".gc_rates").each((i, v) => { 440 v.value = parseFloat(gc_values[i].toFixed(4)); 441 }); 442 }); 443 }, 444 // update_bet(type, value) { 445 // if(type == 'bet_fraction') { 446 // if ("/" in value) { 447 // let v = value.split("/") 448 // this.bet_base = parseFloat(v[0]) / parseFloat(v[1]) 449 // } 450 // } 451 // else if(type == 'bet_decimal') { 452 // this.bet_base = parseFloat(value) 453 // } 454 // else if(type == 'bet_american') { 455 // if (value < 0) { 456 // this.bet_base = 100/(-value)+1 457 // } 458 // else { 459 // this.bet_base = value/100+1 460 // } 461 // } 462 // } 463 update_goal(e) { 464 let order = e.currentTarget.dataset.order; 465 let rate = e.currentTarget.value; 466 let val = this.lambda_to_percentage(rate); 467 this.players[order].goal = parseFloat((100 - 100 * val).toFixed(3)); 468 this.$forceUpdate(); 469 }, 470 update_assist(e) { 471 let order = e.currentTarget.dataset.order; 472 let rate = e.currentTarget.value; 473 let val = this.lambda_to_percentage(rate); 474 this.players[order].assist = parseFloat((100 - 100 * val).toFixed(3)); 475 this.$forceUpdate(); 476 }, 477 update_gc(e) { 478 let order = e.currentTarget.dataset.order; 479 let rate = e.currentTarget.value; 480 let val = this.lambda_to_percentage(rate); 481 this.players[order].cs = parseFloat((100 * val).toFixed(3)); 482 this.$forceUpdate(); 483 }, 484 }, 485}); 486 487function draw_ev_graph(data, avg_value, player_names, colors) { 488 var margin = { top: 15, bottom: 20, left: 20, right: 50 }, 489 width = 400 - margin.left - margin.right, 490 height = 150 - margin.top - margin.bottom; 491 492 let is_mobile = window.screen.width < 800; 493 494 let font_size = "4pt"; 495 let title_size = "4.5pt"; 496 497 if (is_mobile) { 498 height = 180 - margin.top - margin.bottom; 499 font_size = "6.5pt"; 500 title_size = "7pt"; 501 } 502 503 let cnv = d3 504 .select("#ev_graph") 505 .append("svg") 506 .attr( 507 "viewBox", 508 `0 0 ${width + margin.left + margin.right} ${ 509 height + margin.top + margin.bottom 510 }` 511 ) 512 .style("display", "block"); 513 514 let svg = cnv 515 .append("g")
516 .attr("class", "svg-actual") 517 .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 518 let content = svg.append("g").attr("id", "graph-content"); 519 let grayrect = content.append("g").attr("class", "brush"); 520 const themeColors = getThemeColors(); 521 grayrect 522 .append("rect") 523 .attr("fill", themeColors.svgBg) 524 .attr("width", width) 525 .attr("height", height) 526 .attr("stroke", themeColors.svgStroke) 527 .attr("stroke-width", "0.5"); 528 529 data.forEach((d, i) => { 530 d.forEach((j) => { 531 j.player_no = i; 532 }); 533 }); 534 535 // Min max values 536 let x_high = Math.max(...data.map((i) => i.map((j) => j.points)).flat()); 537 let x_low = Math.min(...data.map((i) => i.map((j) => j.points)).flat()); 538 let x_domain = _.range(x_low, x_high + 1); 539 540 // Axis-x 541 var x = d3 542 .scaleBand() 543 .domain(x_domain) 544 .range([0, width]) 545 .paddingInner(0.3) 546 .paddingOuter(0.1); 547 let xAxis = svg 548 .append("g") 549 .attr("opacity", 1) 550 .attr("transform", `translate(0, ${height})`) 551 .call(d3.axisBottom(x).tickSize(0)); 552 553 let players = _.range(data.length); 554 555 var x_sub = d3 556 .scaleBand() 557 .domain(players) 558 .range([0, x.bandwidth()]) 559 .paddingInner(0) 560 .paddingOuter(0); 561 562 // Add X axis label: 563 svg 564 .append("text") 565 .attr("text-anchor", "middle") 566 .attr("x", width / 2) 567 .attr("y", height + 20) 568 .attr("font-size", font_size) 569 .attr("fill", themeColors.svgText) 570 .text("Points"); 571 572 // Axis-y 573 let y_high = Math.max( 574 Math.max(...data.map((i) => i.map((j) => j.probability)).flat()) * 1.1, 575 0.5 576 ); 577 let y_low = 0; 578 var y = d3.scaleLinear().domain([y_low, y_high]).range([height, 0]); 579 let yAxis = svg 580 .append("g") 581 .attr("transform", "translate(" + width + ",0)") 582 .call(d3.axisLeft(y).tickSize(width)) 583 .call((g) => g.selectAll(".tick text")) 584 .call((g) => 585 g.selectAll(".tick:first-of-type line").style("display", "none") 586 ); 587 588 svg 589 .call((g) => g.selectAll(".tick text").attr("fill", themeColors.svgText)) 590 .call((g) => 591 g 592 .selectAll(".tick line") 593 .attr("stroke-dasharray", "3,1") 594 .attr("stroke-width", 0.5) 595 .attr("stroke-opacity", 0.1) 596 .style("pointer-events", "none") 597 ) 598 .call((g) => g.selectAll(".domain").attr("opacity", 0)); 599 600 // Add y axis label: 601 svg 602 .append("text") 603 .attr("text-anchor", "left") 604 .attr("x", -margin.left) 605 .attr("y", -5) 606 .attr("font-size", font_size) 607 .attr("fill", themeColors.svgText) 608 .text("Probability %"); 609 610 svg.call((g) => g.selectAll(".tick").style("font-size", font_size)); 611 svg.call((g) => g.selectAll(".domain").attr("opacity", 0)); 612 613 let ev_title = svg 614 .append("text") 615 .attr("text-anchor", "middle") 616 .attr("alignment-baseline", "center") 617 .attr("dominant-baseline", "center") 618 .attr("x", width / 2) 619 .attr("y", -8) 620 .attr("font-size", title_size) 621 .attr("fill", themeColors.svgText) 622 .text("Point Probability Distribution"); // + (name != '' ? ` (${name})` : '')); 623 624 // Data plot 625 626 let holder = svg.append("g"); 627 628 let flat_data = data.flat(); 629 630 let bars = holder.selectAll().data(flat_data); 631 // let points = holder.selectAll().data(data) 632 633 let bar_entries = bars.enter().append("rect"); 634 bar_entries 635 .attr("class", "probability-bars") 636 .attr("fill", (d) => colors(d.player_no)) 637 // .attr("fill-opacity", 0.5) 638 .attr("stroke", themeColors.svgStroke) 639 .attr("stroke-width", 0.5) 640 .attr("x", (d) => x(d.points) + x_sub(d.player_no)) 641 .attr("y", (d) => y(d.probability)) 642 .attr("width", x_sub.bandwidth()) 643 .attr("height", (d) => y(0) - y(d.probability)); 644 645 let avg_grp = svg.append("g").selectAll(".avg_lines").data(avg_value).enter(); 646 let together = avg_grp.append("g").attr("class", "avg_lines"); 647 let avg_place = (e) => 648 x(Math.floor(e)) + x.step() * (e - Math.floor(e)) + x.bandwidth() / 2; 649 let avg_line = together 650 .append("line") 651 .attr("x1", (d) => avg_place(d)) 652 .attr("x2", (d) => avg_place(d)) 653 .attr("y1", y(y_high)) 654 .attr("y2", y(y_low)) 655 .attr("stroke", themeColors.svgStroke); 656 let avg_text = together 657 .append("text") 658 .attr("text-anchor", "middle") 659 .attr("alignment-baseline", "baseline") 660 .attr("dominant-baseline", "baseline") 661 .attr("x", (d) => avg_place(d)) 662 .attr("y", -1) 663 .attr("font-size", title_size) 664 .attr("fill", (d, i) => colors(i)) 665 .text((d) => d.toFixed(2)); 666 667 let legend = svg 668 .append("g") 669 .attr("transform", "translate(" + (width + 5) + ",0)"); 670 let legend_box = legend 671 .append("rect") 672 .attr("x", 0) 673 .attr("y", 0) 674 .attr("width", 40) 675 .attr("height", 7 * (player_names.length + 0.5)) 676 .attr("fill", "#5a5d5c") 677 .attr("stroke", themeColors.svgStroke) 678 .attr("stroke-width", 0.5); 679 let legend_entry = legend.selectAll().data(player_names).enter().append("g"); 680 legend_entry 681 .append("rect") 682 .attr("class", "legend-rect") 683 .attr("x", 2) 684 .attr("y", (d, i) => 3.5 + 7 * i) 685 .attr("width", 10) 686 .attr("height", 3) 687 .attr("fill", (d, i) => colors(i)) 688 .attr("stroke", themeColors.svgStroke) 689 .attr("stroke-width", 0.5); 690 legend_entry 691 .append("text") 692 .attr("class", "legend-text") 693 .attr("text-anchor", "start") 694 .attr("alignment-baseline", "middle") 695 .attr("dominant-baseline", "middle") 696 .attr("x", 14) 697 .attr("y", (d, i) => 2 + 7 * i + 3) 698 .attr("font-size", "4pt") 699 .attr("fill", themeColors.svgText) 700 .text((d) => d); 701 702 let update_func = (new_data, new_avg, new_names) => { 703 new_data.forEach((d, i) => { 704 d.forEach((j) => { 705 j.player_no = i; 706 }); 707 }); 708 709 let new_x_high = Math.max( 710 ...new_data.map((i) => i.map((j) => j.points)).flat() 711 ); 712 let new_x_low = Math.min( 713 ...new_data.map((i) => i.map((j) => j.points)).flat() 714 ); 715 let new_x_domain = _.range(new_x_low, new_x_high + 1); 716 717 // ev_title.text("Point Probability Distribution") 718 719 legend_box.attr("height", 7 * (new_names.length + 0.5)); 720 let new_legend_rect = legend.selectAll(".legend-rect").data(new_names); 721 new_legend_rect.exit().remove(); 722 new_legend_rect 723 .enter() 724 .append("rect") 725 .attr("class", "legend-rect") 726 .attr("x", 2) 727 .attr("y", (d, i) => 3.5 + 7 * i) 728 .attr("width", 10) 729 .attr("height", 3) 730 .attr("fill", (d, i) => colors(i)) 731 .attr("stroke", themeColors.svgStroke) 732 .attr("stroke-width", 0.5); 733 let new_legend_text = legend.selectAll(".legend-text").data(new_names); 734 new_legend_text.exit().remove(); 735 new_legend_text 736 .attr("x", 14) 737 .attr("y", (d, i) => 2 + 7 * i + 3) 738 .text((d) => d); 739 new_legend_text 740 .enter() 741 .append("text") 742 .attr("class", "legend-text") 743 .attr("text-anchor", "start") 744 .attr("alignment-baseline", "middle") 745 .attr("dominant-baseline", "middle") 746 .attr("x", 14) 747 .attr("y", (d, i) => 2 + 7 * i + 3) 748 .attr("font-size", "4pt") 749 .attr("fill", themeColors.svgText) 750 .text((d) => d); 751 752 x.domain(new_x_domain); 753 xAxis.transition().duration(1000).call(d3.axisBottom(x).tickSize(0)); 754 755 let new_y_high = Math.max( 756 Math.max(...new_data.map((i) => i.map((j) => j.probability)).flat()) * 757 1.1, 758 0.5 759 ); 760 y.domain([0, new_y_high]); 761 yAxis 762 .transition() 763 .duration(1000) 764 .call(d3.axisLeft(y).tickSize(width)) 765 .call((g) => g.selectAll(".tick text")) 766 .call((g) => 767 g.selectAll(".tick:first-of-type line").style("display", "none") 768 ); 769 770 let new_players = _.range(new_data.length); 771 772 x_sub 773 .domain(new_players) 774 .range([0, x.bandwidth()]) 775 .paddingInner(0) 776 .paddingOuter(0); 777 778 // bar_entries 779 // .data(new_data) 780 // .transition().duration(1000) 781 // .attr('y', (d) => y(d.probability)) 782 // .attr("height", (d) => y(0)-y(d.probability)) 783 784 // bar_entries.remove() 785 786 let flat_new_data = new_data.flat(); 787 788 let new_bars = svg.selectAll(".probability-bars").data(flat_new_data); 789 new_bars.exit().remove(); 790 791 new_bars 792 .transition() 793 .duration(1000) 794 .attr("fill", (d) => colors(d.player_no)) 795 .attr("x", (d) => x(d.points) + x_sub(d.player_no)) 796 .attr("y", (d) => y(d.probability)) 797 .attr("width", x_sub.bandwidth()) 798 .attr("height", (d) => y(0) - y(d.probability)); 799 800 new_bars 801 .enter() 802 .append("rect") 803 .attr("x", (d) => x(d.points) + x_sub(d.player_no)) 804 .attr("y", (d) => y(d.probability)) 805 .attr("width", x_sub.bandwidth()) 806 .attr("height", (d) => y(0) - y(d.probability)) 807 .attr("class", "probability-bars") 808 .attr("fill", (d) => colors(d.player_no)) 809 .attr("stroke", themeColors.svgStroke) 810 .attr("stroke-width", 0.5); 811 812 // .attr("fill", "#f4afff") 813 814 // AXIS UPDATE 815 816 svg 817 .call((g) => g.selectAll(".tick text").attr("fill", themeColors.svgText)) 818 .call((g) => 819 g 820 .selectAll(".tick line") 821 .attr("stroke-dasharray", "3,1") 822 .attr("stroke-width", 0.5) 823 .attr("stroke-opacity", 0.1) 824 .style("pointer-events", "none") 825 ) 826 .call((g) => g.selectAll(".domain").attr("opacity", 0)); 827 828 svg.call((g) => g.selectAll(".tick").style("font-size", font_size)); 829 svg.call((g) => g.selectAll(".domain").attr("opacity", 0)); 830 831 let new_avg_grp = svg.selectAll(".avg_lines").data(new_avg); 832 let new_avg_place = (e) => 833 x(Math.floor(e)) + x.step() * (e - Math.floor(e)) + x.bandwidth() / 2; 834 new_avg_grp.exit().remove(); 835 new_avg_grp 836 .transition() 837 .duration(1000) 838 .select("line") 839 .attr("x1", (d) => new_avg_place(d)) 840 .attr("x2", (d) => new_avg_place(d)) 841 .attr("y1", y(new_y_high)) 842 .attr("y2", y(0)) 843 .attr("stroke", themeColors.svgStroke); 844 new_avg_grp 845 .transition() 846 .duration(1000) 847 .select("text") 848 .attr("text-anchor", "middle") 849 .attr("alignment-baseline", "baseline") 850 .attr("dominant-baseline", "baseline") 851 .attr("x", (d) => new_avg_place(d)) 852 .attr("y", -1) 853 .attr("font-size", title_size) 854 .attr("fill", (d, i) => colors(i)) 855 .text((d) => d.toFixed(2)); 856 let new_together = new_avg_grp 857 .enter() 858 .append("g") 859 .attr("class", "avg_lines"); 860 new_together 861 .append("line") 862 .attr("x1", (d) => new_avg_place(d)) 863 .attr("x2", (d) => new_avg_place(d)) 864 .attr("y1", y(new_y_high)) 865 .attr("y2", y(0)) 866 .attr("stroke", themeColors.svgStroke); 867 new_together 868 .append("text") 869 .attr("text-anchor", "middle") 870 .attr("alignment-baseline", "baseline") 871 .attr("dominant-baseline", "baseline") 872 .attr("x", (d) => new_avg_place(d)) 873 .attr("y", -1) 874 .attr("font-size", title_size) 875 .attr("fill", (d, i) => colors(i)) 876 .text((d) => d.toFixed(2)); 877 878 // let new_avg_place = x(Math.floor(new_avg)) + x.step()*(new_avg-Math.floor(new_avg)) + x.bandwidth()/2 879 // avg_line.transition().duration(1000) 880 // .attr("x1", new_avg_place) 881 // .attr("x2", new_avg_place) 882 // .attr("y1", y(new_y_high)) 883 // .attr("y2", y(0)) 884 // avg_text.transition().duration(1000) 885 // .attr("x", new_avg_place) 886 // .text(new_avg.toFixed(2)); 887 }; 888 return update_func; 889} 890
891function draw_generic({ selector, title, x_title, y_title, data, color } = {}) { 892 var margin = { top: 20, bottom: 20, left: 20, right: 10 }, 893 width = 400 - margin.left - margin.right, 894 height = 150 - margin.top - margin.bottom; 895 896 let is_mobile = window.screen.width < 800; 897 898 let font_size = "4pt"; 899 let title_size = "4.5pt"; 900 901 if (is_mobile) { 902 height = 180 - margin.top - margin.bottom; 903 font_size = "6.5pt"; 904 title_size = "7pt"; 905 } 906 907 jQuery(selector).empty(); 908 909 let cnv = d3 910 .select(selector) 911 .append("svg") 912 .attr( 913 "viewBox", 914 `0 0 ${width + margin.left + margin.right} ${ 915 height + margin.top + margin.bottom 916 }` 917 ) 918 .style("display", "block"); 919 920 let svg = cnv 921 .append("g") 922 .attr("class", "svg-actual") 923 .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); 924 let content = svg.append("g").attr("id", "graph-content"); 925 let grayrect = content.append("g").attr("class", "brush"); 926 const themeColors = getThemeColors(); 927 grayrect 928 .append("rect") 929 .attr("fill", themeColors.svgBg) 930 .attr("width", width) 931 .attr("height", height) 932 .attr("stroke", themeColors.svgStroke) 933 .attr("stroke-width", "0.5"); 934 935 // Min max values 936 let x_domain = data.map((i) => i.count); 937 938 // Axis-x 939 var x = d3 940 .scaleBand() 941 .domain(x_domain) 942 .range([0, width]) 943 .paddingInner(0.3) 944 .paddingOuter(0.1); 945 let xAxis = svg 946 .append("g") 947 .attr("opacity", 1) 948 .attr("transform", `translate(0, ${height})`) 949 .call(d3.axisBottom(x).tickSize(0)); 950 951 // Add X axis label: 952 svg 953 .append("text") 954 .attr("text-anchor", "middle") 955 .attr("x", width / 2) 956 .attr("y", height + 20) 957 .attr("font-size", font_size) 958 .attr("fill", themeColors.svgText) 959 .text(x_title); 960 961 // Axis-y 962 let y_high = Math.max(Math.max(...data.map((i) => i.probability)) * 1.1, 0.5); 963 let y_low = 0; 964 var y = d3.scaleLinear().domain([y_low, y_high]).range([height, 0]); 965 let yAxis = svg 966 .append("g") 967 .attr("transform", "translate(" + width + ",0)") 968 .call(d3.axisLeft(y).tickSize(width)) 969 .call((g) => g.selectAll(".tick text")) 970 .call((g) => 971 g.selectAll(".tick:first-of-type line").style("display", "none") 972 ); 973 974 svg 975 .call((g) => g.selectAll(".tick text").attr("fill", themeColors.svgText)) 976 .call((g) => 977 g 978 .selectAll(".tick line") 979 .attr("stroke-dasharray", "3,1") 980 .attr("stroke-width", 0.5) 981 .attr("stroke-opacity", 0.1) 982 .style("pointer-events", "none") 983 ) 984 .call((g) => g.selectAll(".domain").attr("opacity", 0)); 985 986 // Add y axis label: 987 svg 988 .append("text") 989 .attr("text-anchor", "left") 990 .attr("x", -margin.left) 991 .attr("y", -5) 992 .attr("font-size", font_size) 993 .attr("fill", themeColors.svgText) 994 .text(y_title); 995 996 svg.call((g) => g.selectAll(".tick").style("font-size", font_size)); 997 svg.call((g) => g.selectAll(".domain").attr("opacity", 0)); 998 999 let plot_title = svg 1000 .append("text") 1001 .attr("text-anchor", "middle") 1002 .attr("alignment-baseline", "center") 1003 .attr("dominant-baseline", "center") 1004 .attr("x", width / 2) 1005 .attr("y", -8) 1006 .attr("font-size", title_size) 1007 .attr("fill", themeColors.svgText) 1008 .text(title); 1009 1010 let holder = svg.append("g"); 1011 1012 let bars = holder.selectAll().data(data); 1013 let points = holder.selectAll().data(data); 1014 1015 let bar_entries = bars.enter().append("rect"); 1016 bar_entries 1017 .attr("class", "probability-bars") 1018 .attr("fill", color) 1019 .attr("x", (d) => x(d.count)) 1020 .attr("y", (d) => y(d.probability)) 1021 .attr("width", x.bandwidth()) 1022 .attr("height", (d) => y(0) - y(d.probability)); 1023 1024 let point_entries = points.enter().append("text"); 1025 point_entries 1026 .text((d) => d.points + " Pts") 1027 .attr("class", "prob-bar-values") 1028 .attr("x", (d) => x(d.count) + x.bandwidth() / 2) 1029 .attr("text-anchor", "middle") 1030 .attr("y", (d) => y(d.probability) - 2) 1031 .attr("alignment-baseline", "baseline") 1032 .attr("dominant-baseline", "baseline") 1033 .attr("font-size", font_size) 1034 .attr("fill", themeColors.svgText); 1035 1036 let update_func = (new_data, new_name) => { 1037 let new_y_high = Math.max( 1038 Math.max(...new_data.map((i) => i.probability)) * 1.1, 1039 0.5 1040 ); 1041 y.domain([0, new_y_high]); 1042 yAxis 1043 .transition() 1044 .duration(1000) 1045 .call(d3.axisLeft(y).tickSize(width)) 1046 .call((g) => g.selectAll(".tick text")) 1047 .call((g) => 1048 g.selectAll(".tick:first-of-type line").style("display", "none") 1049 ); 1050 1051 plot_title.text(title + (new_name != "" ? ` (${new_name})` : "")); 1052 1053 bar_entries 1054 .data(new_data) 1055 .transition() 1056 .duration(1000) 1057 .attr("y", (d) => y(d.probability)) 1058 .attr("height", (d) => y(0) - y(d.probability)); 1059 1060 point_entries 1061 .data(new_data) 1062 .transition() 1063 .duration(1000) 1064 .text((d) => d.points + " Pts") 1065 .attr("y", (d) => y(d.probability) - 2); 1066 1067 // AXIS UPDATE 1068 1069 svg 1070 .call((g) => g.selectAll(".tick text").attr("fill", themeColors.svgText)) 1071 .call((g) => 1072 g 1073 .selectAll(".tick line") 1074 .attr("stroke-dasharray", "3,1") 1075 .attr("stroke-width", 0.5) 1076 .attr("stroke-opacity", 0.1) 1077 .style("pointer-events", "none") 1078 ) 1079 .call((g) => g.selectAll(".domain").attr("opacity", 0)); 1080 1081 svg.call((g) => g.selectAll(".tick").style("font-size", font_size)); 1082 svg.call((g) => g.selectAll(".domain").attr("opacity", 0)); 1083 }; 1084 return update_func; 1085} 1086 1087$(document).ready(() => { 1088 app.calculate(); // initial load 1089});
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.