From: Steinar H. Gunderson Date: Wed, 10 May 2023 15:46:50 +0000 (+0200) Subject: Add a rudimentary start of the viewer. X-Git-Url: https://git.sesse.net/?a=commitdiff_plain;h=2b0515d20dd32fafa4f8966921837fefb8e27394;p=pkanalytics Add a rudimentary start of the viewer. --- diff --git a/ultimate.js b/ultimate.js new file mode 100644 index 0000000..b65a2b3 --- /dev/null +++ b/ultimate.js @@ -0,0 +1,231 @@ +'use strict'; + +// No frameworks, no compilers, no npm, just JavaScript. :-) + +let json; + +fetch('ultimate.json') + .then(response => response.json()) + .then(response => { process_matches(response); }); + +function attribute_player_time(player, to, from) { + if (player.on_field_since > from) { + // Player came in while play happened (without a stoppage!?). + player.playing_time_ms += to - player.on_field_since; + } else { + player.playing_time_ms += to - from; + } +} + +function take_off_field(player, t, live_since) { + if (live_since === null) { + // Play isn't live, so nothing to do. + } else { + attribute_player_time(player, t, live_since); + } + player.on_field_since = null; +} + +function add_cell(tr, element_type, text) { + let element = document.createElement(element_type); + element.textContent = text; + tr.appendChild(element); +} + +function process_matches(json) { + let players = {}; + for (const player of json['players']) { + players[player['player_id']] = { + 'name': player['name'], + 'number': player['number'], + + 'goals': 0, + 'assists': 0, + 'hockey_assists': 0, + 'catches': 0, + 'touches': 0, + 'num_throws': 0, + 'throwaways': 0, + 'drops': 0, + + 'defenses': 0, + 'interceptions': 0, + 'points_played': 0, + 'playing_time_ms': 0, + + 'offensive_soft_plus': 0, + 'offensive_soft_minus': 0, + 'defensive_soft_plus': 0, + 'defensive_soft_minus': 0, + + 'pulls': 0, + 'pull_times': [], + 'oob_pulls': 0, + + // Internal. + 'last_point_seen': null, + 'on_field_since': null, + }; + } + + for (const match of json['matches']) { + let handler = null; + let prev_handler = null; + let live_since = null; + let offense = null; + let puller = null; + let pull_started = null; + let point_num = 0; + for (const [q,p] of Object.entries(players)) { + p.on_field_since = null; + p.last_point_seen = null; + } + for (const e of match['events']) { + let t = e['t']; + let type = e['type']; + let p = players[e['player']]; + + // Point count management + if (p !== undefined && type !== 'out' && p.last_point_seen !== point_num) { + p.last_point_seen = point_num; + ++p.points_played; + } + if (type === 'goal' || type === 'their_goal') { + ++point_num; + } + + // Sub management + if (type === 'in' && p.on_field_since === null) { + p.on_field_since = t; + } else if (type === 'out') { + take_off_field(p, t, live_since); + } + + // Liveness management + if (type === 'pull' || type === 'their_pull' || type === 'restart') { + live_since = t; + } else if (type === 'goal' || type === 'their_goal' || type === 'stoppage') { + for (const [q,p] of Object.entries(players)) { + if (p.on_field_since !== null) { + attribute_player_time(p, t, live_since); + } + } + live_since = null; + } + + // Pull management + if (type === 'pull') { + puller = e['player']; + pull_started = t; + ++p.pulls; + } else if (type === 'in' || type === 'out' || type === 'stoppage' || type === 'restart' || type === 'unknown' || type === 'set_defense' || type === 'set_offense') { + // No effect on pull. + } else if (type === 'pull_landed' && puller !== null) { + players[puller].pull_times.push(t - pull_started); + } else if (type === 'pull_oob' && puller !== null) { + ++players[puller].oob_pulls; + } else { + // Not pulling (if there was one, we never recorded its outcome, but still count it). + puller = pull_started = null; + } + + // Offense/defense management (TODO: use it for actual counting) + if (type === 'set_defense' || type === 'goal' || type === 'throwaway' || type === 'drop') { + offense = false; + } else if (type === 'set_offense' || type === 'their_goal' || type === 'their_throwaway' || type === 'defense' || type === 'interception') { + offense = true; + } + + // Event management + if (type === 'catch' || type === 'goal') { + if (handler !== null) { + ++players[handler].num_throws; + ++p.catches; + } + + ++p.touches; + if (type === 'goal') { + if (prev_handler !== null) { + ++players[prev_handler].hockey_assists; + } + if (handler !== null) { + ++players[handler].assists; + } + ++p.goals; + handler = prev_handler = null; + } else { + // Update hold history. + prev_handler = handler; + handler = e['player']; + } + } else if (type === 'throwaway') { + ++p.throwaways; + handler = prev_handler = null; + } else if (type === 'drop') { + ++p.drops; + handler = prev_handler = null; + } else if (type === 'defense') { + ++p.defenses; + } else if (type === 'interception') { + ++p.interceptions; + ++p.defenses; + ++p.touches; + prev_handler = null; + handler = e['player']; + } else if (type === 'offensive_soft_plus' || type === 'offensive_soft_minus' || type === 'defensive_soft_plus' || type === 'defensive_soft_minus') { + ++p[type]; + } else if (type !== 'in' && type !== 'out' && type !== 'pull' && + type !== 'their_pull' && type !== 'restart' && type !== 'goal' && + type !== 'their_goal' && type !== 'stoppage' && type !== 'unknown' && + type !== 'set_defense' && type !== 'goal' && type !== 'throwaway' && + type !== 'drop' && type !== 'set_offense' && type !== 'their_goal' && + type !== 'pull' && type !== 'pull_landed' && type !== 'pull_oob' && + type !== 'their_throwaway' && type !== 'defense' && type !== 'interception') { + console.log("Unknown event:", e); + } + } + } + + 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', 'Points played'); + add_cell(header, 'th', 'Time played'); + rows.push(header); + } + + for (const [q,p] of Object.entries(players)) { + let row = document.createElement('tr'); + let pm = p.goals + p.assists + p.hockey_assists + p.defenses - p.throwaways - p.drops; + let soft_pm = p.offensive_soft_plus + p.defensive_soft_plus - p.offensive_soft_minus - p.defensive_soft_minus; + add_cell(row, 'td', p.name); // TODO: number? + add_cell(row, 'td', pm > 0 ? ('+' + pm) : pm); + add_cell(row, 'td', soft_pm > 0 ? ('+' + soft_pm) : soft_pm); + add_cell(row, 'td', p.points_played); + add_cell(row, 'td', Math.floor(p.playing_time_ms / 60000) + ' min'); + rows.push(row); + console.log(p.name + " played " + p.points_played + " points (" + Math.floor(p.playing_time_ms / 60000) + " min), " + p.goals + " goals, " + p.assists + " assists, plus/minus: " + pm); + } + document.getElementById('stats').replaceChildren(...rows); + + console.log("PULL STATS"); + for (const [q,p] of Object.entries(players)) { + if (p.pulls === 0) { + continue; + } + let sum_time = 0; + for (const t of p.pull_times) { + sum_time += t; + } + let avg_time = 1e-3 * sum_time / p.pulls; + let msg = p.name + ' did ' + p.pulls + ' pull(s), ' + p.oob_pulls + ' OOB'; + if (p.oob_pulls < p.pulls) { + msg += ', avg. hangtime ' + avg_time.toFixed(1) + ' sec for others'; + } + console.log(msg, p.pull_times); + } +} diff --git a/viewer.html b/viewer.html new file mode 100644 index 0000000..569f696 --- /dev/null +++ b/viewer.html @@ -0,0 +1,14 @@ + + + + + Plastkast Analytics + + + + + +
+ + +