PageSourceSearch

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

js fploptimized.com collected 2026-10-04 13:32:12 UTC 33,178 bytes, 1,089 lines download raw bytes

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.