]> git.sesse.net Git - pkanalytics/blobdiff - ultimate.js
Make soft +/- span both columns.
[pkanalytics] / ultimate.js
index 8da3c244da5a2c0009d90911ad338e3231d8cfff..3ff7de822afd50a13d052fe9e773d036615acaaa 100644 (file)
@@ -33,10 +33,17 @@ function take_off_field(player, t, live_since) {
 function add_cell(tr, element_type, text) {
        let element = document.createElement(element_type);
        element.textContent = text;
-       if (element_type === 'th') {
+       tr.appendChild(element);
+       return element;
+}
+
+function add_th(tr, text, colspan) {
+       let element = add_cell(tr, 'th', text);
+       if (colspan > 0) {
+               element.setAttribute('colspan', colspan);
+       } else {
                element.setAttribute('colspan', '3');
        }
-       tr.appendChild(element);
        return element;
 }
 
@@ -55,11 +62,22 @@ function add_3cell(tr, text, cls) {
        return element;
 }
 
+function add_3cell_with_filler_ci(tr, text, cls) {
+       let element = add_3cell(tr, text, cls);
+
+       let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
+       svg.classList.add('fillerci');
+       svg.setAttribute('width', ci_width);
+       svg.setAttribute('height', ci_height);
+       element.appendChild(svg);
+
+       return element;
+}
+
 function add_3cell_ci(tr, ci) {
-       console.log(ci);
        if (isNaN(ci.val)) {
-               add_3cell(tr, 'N/A');
-               return;  // FIXME: some SVG padding needed
+               add_3cell_with_filler_ci(tr, 'N/A');
+               return;
        }
 
        let text;
@@ -69,46 +87,48 @@ function add_3cell_ci(tr, ci) {
                text = ci.val.toFixed(2);
        }
        let element = add_3cell(tr, text);
-       let to_x = (val) => { return width * (val - ci.min) / (ci.max - ci.min); };
+       let to_x = (val) => { return ci_width * (val - ci.min) / (ci.max - ci.min); };
 
        // Container.
-       const width = 100;
-       const height = 20;
        let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
-       svg.classList.add('ci');
-       svg.setAttribute('width', width);
-       svg.setAttribute('height', height);
+       if (ci.inverted === true) {
+               svg.classList.add('invertedci');
+       } else {
+               svg.classList.add('ci');
+       }
+       svg.setAttribute('width', ci_width);
+       svg.setAttribute('height', ci_height);
 
        // The good (green) and red (bad) ranges.
        let s0 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
        s0.classList.add('range');
        s0.classList.add('s0');
        s0.setAttribute('width', to_x(ci.desired));
-       s0.setAttribute('height', height);
+       s0.setAttribute('height', ci_height);
        s0.setAttribute('x', '0');
 
        let s1 = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
        s1.classList.add('range');
        s1.classList.add('s1');
-       s1.setAttribute('width', width - to_x(ci.desired));
-       s1.setAttribute('height', height);
+       s1.setAttribute('width', ci_width - to_x(ci.desired));
+       s1.setAttribute('height', ci_height);
        s1.setAttribute('x', to_x(ci.desired));
 
        // Confidence bar.
        let bar = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
        bar.classList.add('bar');
        bar.setAttribute('width', to_x(ci.upper_ci) - to_x(ci.lower_ci));
-       bar.setAttribute('height', height / 3);
+       bar.setAttribute('height', ci_height / 3);
        bar.setAttribute('x', to_x(ci.lower_ci));
-       bar.setAttribute('y', height / 3);
+       bar.setAttribute('y', ci_height / 3);
 
        // Marker line for average.
        let marker = document.createElementNS('http://www.w3.org/2000/svg', 'line');
        marker.classList.add('marker');
        marker.setAttribute('x1', to_x(ci.val));
        marker.setAttribute('x2', to_x(ci.val));
-       marker.setAttribute('y1', height / 6);
-       marker.setAttribute('y2', height * 5 / 6);
+       marker.setAttribute('y1', ci_height / 6);
+       marker.setAttribute('y2', ci_height * 5 / 6);
 
        svg.appendChild(s0);
        svg.appendChild(s1);
@@ -481,6 +501,9 @@ function write_main_menu(chosen_category) {
 // https://en.wikipedia.org/wiki/1.96#History
 const z = 1.959964;
 
+const ci_width = 100;
+const ci_height = 20;
+
 function make_binomial_ci(val, num, z) {
        let avg = val / num;
 
@@ -514,16 +537,42 @@ function make_efficiency_ci(points_won, points_completed, z)
        return ci;
 }
 
+// Ds, throwaways and drops can happen multiple times per point,
+// so they are Poisson distributed.
+//
+// Modified Wald (recommended by http://www.ine.pt/revstat/pdf/rs120203.pdf
+// since our rates are definitely below 2 per point).
+function make_poisson_ci(val, num, z, inverted)
+{
+       let low  = (val == 0) ? 0.0 : ((val - 0.5) - Math.sqrt(val - 0.5)) / num;
+       let high = (val == 0) ? -Math.log(0.025) / num : ((val + 0.5) + Math.sqrt(val + 0.5)) / num;
+
+       // Fix the signs so that we don't get -0.00.
+       low = Math.max(low, 0.0);
+
+       // The display range of 0 to 0.25 is fairly arbitrary. So is the desired 0.05 per point.
+       let avg = val / num;
+       return {
+               'val': avg,
+               'lower_ci': low,
+               'upper_ci': high,
+               'min': 0.0,
+               'max': 0.25,
+               'desired': 0.05,
+               'inverted': inverted,
+       };
+}
+
 function make_table_general(players) {
        let rows = [];
        {
                let header = document.createElement('tr');
-               add_cell(header, 'th', 'Player');
-               add_cell(header, 'th', '+/-');
-               add_cell(header, 'th', 'Soft +/-');
-               add_cell(header, 'th', 'O efficiency');
-               add_cell(header, 'th', 'D efficiency');
-               add_cell(header, 'th', 'Points played');
+               add_th(header, 'Player');
+               add_th(header, '+/-');
+               add_th(header, 'Soft +/-');
+               add_th(header, 'O efficiency');
+               add_th(header, 'D efficiency');
+               add_th(header, 'Points played');
                rows.push(header);
        }
 
@@ -563,17 +612,17 @@ function make_table_offense(players) {
        let rows = [];
        {
                let header = document.createElement('tr');
-               add_cell(header, 'th', 'Player');
-               add_cell(header, 'th', 'Goals');
-               add_cell(header, 'th', 'Assists');
-               add_cell(header, 'th', 'Hockey assists');
-               add_cell(header, 'th', 'Throws');
-               add_cell(header, 'th', 'Throwaways');
-               add_cell(header, 'th', '%OK');
-               add_cell(header, 'th', 'Catches');
-               add_cell(header, 'th', 'Drops');
-               add_cell(header, 'th', '%OK');
-               add_cell(header, 'th', 'Soft +/-');
+               add_th(header, 'Player');
+               add_th(header, 'Goals');
+               add_th(header, 'Assists');
+               add_th(header, 'Hockey assists');
+               add_th(header, 'Throws');
+               add_th(header, 'Throwaways');
+               add_th(header, '%OK');
+               add_th(header, 'Catches');
+               add_th(header, 'Drops');
+               add_th(header, '%OK');
+               add_th(header, 'Soft +/-', 6);
                rows.push(header);
        }
 
@@ -644,12 +693,12 @@ function make_table_defense(players) {
        let rows = [];
        {
                let header = document.createElement('tr');
-               add_cell(header, 'th', 'Player');
-               add_cell(header, 'th', 'Ds');
-               add_cell(header, 'th', 'Pulls');
-               add_cell(header, 'th', 'OOB pulls');
-               add_cell(header, 'th', 'Avg. hang time (IB)');
-               add_cell(header, 'th', 'Soft +/-');
+               add_th(header, 'Player');
+               add_th(header, 'Ds');
+               add_th(header, 'Pulls');
+               add_th(header, 'OOB pulls');
+               add_th(header, 'Avg. hang time (IB)');
+               add_th(header, 'Soft +/-', 6);
                rows.push(header);
        }
        for (const [q,p] of Object.entries(players)) {
@@ -686,12 +735,12 @@ function make_table_playing_time(players) {
        let rows = [];
        {
                let header = document.createElement('tr');
-               add_cell(header, 'th', 'Player');
-               add_cell(header, 'th', 'Points played');
-               add_cell(header, 'th', 'Time played');
-               add_cell(header, 'th', 'Time on field');
-               add_cell(header, 'th', 'O points');
-               add_cell(header, 'th', 'D points');
+               add_th(header, 'Player');
+               add_th(header, 'Points played');
+               add_th(header, 'Time played');
+               add_th(header, 'Time on field');
+               add_th(header, 'O points');
+               add_th(header, 'D points');
                rows.push(header);
        }
 
@@ -725,14 +774,14 @@ function make_table_per_point(players) {
        let rows = [];
        {
                let header = document.createElement('tr');
-               add_cell(header, 'th', 'Player');
-               add_cell(header, 'th', 'Goals');
-               add_cell(header, 'th', 'Assists');
-               add_cell(header, 'th', 'Hockey assists');
-               add_cell(header, 'th', 'Ds');
-               add_cell(header, 'th', 'Throwaways');
-               add_cell(header, 'th', 'Drops');
-               add_cell(header, 'th', 'Touches');
+               add_th(header, 'Player');
+               add_th(header, 'Goals');
+               add_th(header, 'Assists');
+               add_th(header, 'Hockey assists');
+               add_th(header, 'Ds');
+               add_th(header, 'Throwaways');
+               add_th(header, 'Drops');
+               add_th(header, 'Touches');
                rows.push(header);
        }
 
@@ -745,24 +794,28 @@ function make_table_per_point(players) {
        let touches = 0;
        for (const [q,p] of Object.entries(players)) {
                if (q === 'globals') continue;
+
+               // Can only happen once per point, so these are binomials.
+               let ci_goals = make_binomial_ci(p.goals, p.points_played, z);
+               let ci_assists = make_binomial_ci(p.assists, p.points_played, z);
+               let ci_hockey_assists = make_binomial_ci(p.hockey_assists, p.points_played, z);
+               // Arbitrarily desire at least 10% (not everybody can score or assist).
+               ci_goals.desired = 0.1;
+               ci_assists.desired = 0.1;
+               ci_hockey_assists.desired = 0.1;
+
                let row = document.createElement('tr');
                add_3cell(row, p.name, 'name');  // TODO: number?
+               add_3cell_ci(row, ci_goals);
+               add_3cell_ci(row, ci_assists);
+               add_3cell_ci(row, ci_hockey_assists);
+               add_3cell_ci(row, make_poisson_ci(p.defenses, p.points_played, z));
+               add_3cell_ci(row, make_poisson_ci(p.throwaways, p.points_played, z, true));
+               add_3cell_ci(row, make_poisson_ci(p.drops, p.points_played, z, true));
                if (p.points_played > 0) {
-                       add_3cell(row, p.goals == 0 ? 0 : (p.goals / p.points_played).toFixed(2));
-                       add_3cell(row, p.assists == 0 ? 0 : (p.assists / p.points_played).toFixed(2));
-                       add_3cell(row, p.hockey_assists == 0 ? 0 : (p.hockey_assists / p.points_played).toFixed(2));
-                       add_3cell(row, p.defenses == 0 ? 0 : (p.defenses / p.points_played).toFixed(2));
-                       add_3cell(row, p.throwaways == 0 ? 0 : (p.throwaways / p.points_played).toFixed(2));
-                       add_3cell(row, p.drops == 0 ? 0 : (p.drops / p.points_played).toFixed(2));
                        add_3cell(row, p.touches == 0 ? 0 : (p.touches / p.points_played).toFixed(2));
                } else {
                        add_3cell(row, 'N/A');
-                       add_3cell(row, 'N/A');
-                       add_3cell(row, 'N/A');
-                       add_3cell(row, 'N/A');
-                       add_3cell(row, 'N/A');
-                       add_3cell(row, 'N/A');
-                       add_3cell(row, 'N/A');
                }
                rows.push(row);
 
@@ -780,20 +833,20 @@ function make_table_per_point(players) {
        let row = document.createElement('tr');
        add_3cell(row, '');
        if (globals.points_played > 0) {
-               add_3cell(row, goals == 0 ? 0 : (goals / globals.points_played).toFixed(2));
-               add_3cell(row, assists == 0 ? 0 : (assists / globals.points_played).toFixed(2));
-               add_3cell(row, hockey_assists == 0 ? 0 : (hockey_assists / globals.points_played).toFixed(2));
-               add_3cell(row, defenses == 0 ? 0 : (defenses / globals.points_played).toFixed(2));
-               add_3cell(row, throwaways == 0 ? 0 : (throwaways / globals.points_played).toFixed(2));
-               add_3cell(row, drops == 0 ? 0 : (drops / globals.points_played).toFixed(2));
+               add_3cell_with_filler_ci(row, goals == 0 ? 0 : (goals / globals.points_played).toFixed(2));
+               add_3cell_with_filler_ci(row, assists == 0 ? 0 : (assists / globals.points_played).toFixed(2));
+               add_3cell_with_filler_ci(row, hockey_assists == 0 ? 0 : (hockey_assists / globals.points_played).toFixed(2));
+               add_3cell_with_filler_ci(row, defenses == 0 ? 0 : (defenses / globals.points_played).toFixed(2));
+               add_3cell_with_filler_ci(row, throwaways == 0 ? 0 : (throwaways / globals.points_played).toFixed(2));
+               add_3cell_with_filler_ci(row, drops == 0 ? 0 : (drops / globals.points_played).toFixed(2));
                add_3cell(row, touches == 0 ? 0 : (touches / globals.points_played).toFixed(2));
        } else {
-               add_3cell(row, 'N/A');
-               add_3cell(row, 'N/A');
-               add_3cell(row, 'N/A');
-               add_3cell(row, 'N/A');
-               add_3cell(row, 'N/A');
-               add_3cell(row, 'N/A');
+               add_3cell_with_filler_ci(row, 'N/A');
+               add_3cell_with_filler_ci(row, 'N/A');
+               add_3cell_with_filler_ci(row, 'N/A');
+               add_3cell_with_filler_ci(row, 'N/A');
+               add_3cell_with_filler_ci(row, 'N/A');
+               add_3cell_with_filler_ci(row, 'N/A');
                add_3cell(row, 'N/A');
        }
        rows.push(row);