3 // No frameworks, no compilers, no npm, just JavaScript. :-)
7 addEventListener('hashchange', () => { console.log('heei'); process_matches(global_json); });
9 .then(response => response.json())
10 .then(response => { global_json = response; process_matches(global_json); });
12 function attribute_player_time(player, to, from) {
13 if (player.on_field_since > from) {
14 // Player came in while play happened (without a stoppage!?).
15 player.playing_time_ms += to - player.on_field_since;
17 player.playing_time_ms += to - from;
21 function take_off_field(player, t, live_since) {
22 if (live_since === null) {
23 // Play isn't live, so nothing to do.
25 attribute_player_time(player, t, live_since);
27 player.on_field_since = null;
30 function add_cell(tr, element_type, text) {
31 let element = document.createElement(element_type);
32 element.textContent = text;
33 tr.appendChild(element);
36 function process_matches(json) {
38 for (const player of json['players']) {
39 players[player['player_id']] = {
40 'name': player['name'],
41 'number': player['number'],
58 'offensive_points_completed': 0,
59 'offensive_points_won': 0,
60 'defensive_points_completed': 0,
61 'defensive_points_won': 0,
63 'offensive_soft_plus': 0,
64 'offensive_soft_minus': 0,
65 'defensive_soft_plus': 0,
66 'defensive_soft_minus': 0,
73 'last_point_seen': null,
74 'on_field_since': null,
78 for (const match of json['matches']) {
80 let prev_handler = null;
81 let live_since = null;
84 let pull_started = null;
85 let last_pull_was_ours = null; // Effectively whether we're playing an O or D point (not affected by turnovers).
87 for (const [q,p] of Object.entries(players)) {
88 p.on_field_since = null;
89 p.last_point_seen = null;
91 for (const e of match['events']) {
94 let p = players[e['player']];
97 if (type === 'in' && p.on_field_since === null) {
99 } else if (type === 'out') {
100 take_off_field(p, t, live_since);
103 // Liveness management
104 if (type === 'pull' || type === 'their_pull' || type === 'restart') {
106 } else if (type === 'goal' || type === 'their_goal' || type === 'stoppage') {
107 for (const [q,p] of Object.entries(players)) {
108 if (p.on_field_since === null) {
111 if (p.last_point_seen !== point_num) {
112 // In case the player did nothing this point,
113 // not even subbing in.
114 p.last_point_seen = point_num;
117 attribute_player_time(p, t, live_since);
119 if (last_pull_was_ours === true) { // D point.
120 ++p.defensive_points_completed;
121 if (type === 'goal') {
122 ++p.defensive_points_won;
124 } else if (last_pull_was_ours === false) { // O point.
125 ++p.offensive_points_completed;
126 if (type === 'goal') {
127 ++p.offensive_points_won;
134 // Point count management
135 if (p !== undefined && type !== 'out' && p.last_point_seen !== point_num) {
136 p.last_point_seen = point_num;
139 if (type === 'goal' || type === 'their_goal') {
144 if (type === 'pull') {
145 puller = e['player'];
148 } else if (type === 'in' || type === 'out' || type === 'stoppage' || type === 'restart' || type === 'unknown' || type === 'set_defense' || type === 'set_offense') {
149 // No effect on pull.
150 } else if (type === 'pull_landed' && puller !== null) {
151 players[puller].pull_times.push(t - pull_started);
152 } else if (type === 'pull_oob' && puller !== null) {
153 ++players[puller].oob_pulls;
155 // Not pulling (if there was one, we never recorded its outcome, but still count it).
156 puller = pull_started = null;
159 // Offense/defense management (TODO: use it for actual counting)
160 if (type === 'set_defense' || type === 'goal' || type === 'throwaway' || type === 'drop') {
162 } else if (type === 'set_offense' || type === 'their_goal' || type === 'their_throwaway' || type === 'defense' || type === 'interception') {
165 if (type === 'pull') {
166 last_pull_was_ours = true;
167 } else if (type === 'their_pull') {
168 last_pull_was_ours = false;
169 } else if (type === 'set_offense' && last_pull_was_ours === null) {
170 // set_offense could either be “changed to offense for some reason
171 // we could not express”, or “we started in the middle of a point,
172 // and we are offense”. We assume that if we already saw the pull,
173 // it's the former, and if not, it's the latter; thus, the === null
174 // test above. (It could also be “we set offense before the pull,
175 // so that we get the right button enabled”, in which case it will
176 // be overwritten by the next pull/their_pull event anyway.)
177 last_pull_was_ours = false;
178 } else if (type === 'set_defense' && last_pull_was_ours === null) {
180 last_pull_was_ours = true;
181 } else if (type === 'goal' || type === 'their_goal') {
182 last_pull_was_ours = null;
186 if (type === 'catch' || type === 'goal') {
187 if (handler !== null) {
188 ++players[handler].num_throws;
193 if (type === 'goal') {
194 if (prev_handler !== null) {
195 ++players[prev_handler].hockey_assists;
197 if (handler !== null) {
198 ++players[handler].assists;
201 handler = prev_handler = null;
203 // Update hold history.
204 prev_handler = handler;
205 handler = e['player'];
207 } else if (type === 'throwaway') {
210 handler = prev_handler = null;
211 } else if (type === 'drop') {
213 handler = prev_handler = null;
214 } else if (type === 'defense') {
216 } else if (type === 'interception') {
221 handler = e['player'];
222 } else if (type === 'offensive_soft_plus' || type === 'offensive_soft_minus' || type === 'defensive_soft_plus' || type === 'defensive_soft_minus') {
224 } else if (type !== 'in' && type !== 'out' && type !== 'pull' &&
225 type !== 'their_goal' && type !== 'stoppage' && type !== 'unknown' &&
226 type !== 'set_defense' && type !== 'goal' && type !== 'throwaway' &&
227 type !== 'drop' && type !== 'set_offense' && type !== 'their_goal' &&
228 type !== 'pull' && type !== 'pull_landed' && type !== 'pull_oob' &&
229 type !== 'their_throwaway' && type !== 'defense' && type !== 'interception') {
230 console.log("Unknown event:", e);
235 let chosen_category = get_chosen_category();
236 write_main_menu(chosen_category);
239 if (chosen_category === 'general') {
240 rows = make_table_general(players);
241 } else if (chosen_category === 'offense') {
242 rows = make_table_offense(players);
243 } else if (chosen_category === 'defense') {
244 rows = make_table_defense(players);
246 document.getElementById('stats').replaceChildren(...rows);
249 function get_chosen_category() {
250 if (window.location.hash === '#offense') {
252 } else if (window.location.hash === '#defense') {
259 function write_main_menu(chosen_category) {
261 if (chosen_category === 'general') {
262 elems.push(document.createTextNode('General'));
264 let a = document.createElement('a');
265 a.appendChild(document.createTextNode('General'));
266 a.setAttribute('href', '#general');
270 elems.push(document.createTextNode(' | '));
271 if (chosen_category === 'offense') {
272 elems.push(document.createTextNode('Offense'));
274 let a = document.createElement('a');
275 a.appendChild(document.createTextNode('Offense'));
276 a.setAttribute('href', '#offense');
280 elems.push(document.createTextNode(' | '));
281 if (chosen_category === 'defense') {
282 elems.push(document.createTextNode('Defense'));
284 let a = document.createElement('a');
285 a.appendChild(document.createTextNode('Defense'));
286 a.setAttribute('href', '#defense');
290 document.getElementById('mainmenu').replaceChildren(...elems);
293 function make_table_general(players) {
296 let header = document.createElement('tr');
297 add_cell(header, 'th', 'Player');
298 add_cell(header, 'th', '+/-');
299 add_cell(header, 'th', 'Soft +/-');
300 add_cell(header, 'th', 'O efficiency');
301 add_cell(header, 'th', 'D efficiency');
302 add_cell(header, 'th', 'Points played');
303 add_cell(header, 'th', 'Time played');
307 for (const [q,p] of Object.entries(players)) {
308 let row = document.createElement('tr');
309 let pm = p.goals + p.assists + p.hockey_assists + p.defenses - p.throwaways - p.drops;
310 let soft_pm = p.offensive_soft_plus + p.defensive_soft_plus - p.offensive_soft_minus - p.defensive_soft_minus;
311 let o_efficiency = (p.offensive_points_won / p.offensive_points_completed) * 2 - 1;
312 let d_efficiency = (p.defensive_points_won / p.defensive_points_completed) * 2 - 1;
313 add_cell(row, 'td', p.name); // TODO: number?
314 add_cell(row, 'td', pm > 0 ? ('+' + pm) : pm);
315 add_cell(row, 'td', soft_pm > 0 ? ('+' + soft_pm) : soft_pm);
316 add_cell(row, 'td', p.offensive_points_completed > 0 ? o_efficiency.toFixed(2) : 'N/A');
317 add_cell(row, 'td', p.defensive_points_completed > 0 ? d_efficiency.toFixed(2) : 'N/A');
318 add_cell(row, 'td', p.points_played);
319 add_cell(row, 'td', Math.floor(p.playing_time_ms / 60000) + ' min');
321 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);
326 function make_table_offense(players) {
329 let header = document.createElement('tr');
330 add_cell(header, 'th', 'Player');
331 add_cell(header, 'th', 'Goals');
332 add_cell(header, 'th', 'Assists');
333 add_cell(header, 'th', 'Hockey assists');
334 add_cell(header, 'th', 'Throws');
335 add_cell(header, 'th', 'Throwaways');
336 add_cell(header, 'th', '%OK');
337 add_cell(header, 'th', 'Catches');
338 add_cell(header, 'th', 'Drops');
339 add_cell(header, 'th', '%OK');
340 add_cell(header, 'th', 'Soft +/-');
344 for (const [q,p] of Object.entries(players)) {
345 let throw_ok = 100 * (1 - p.throwaways / p.num_throws);
346 let catch_ok = 100 * (p.catches / (p.catches + p.drops));
348 let row = document.createElement('tr');
349 add_cell(row, 'td', p.name); // TODO: number?
350 add_cell(row, 'td', p.goals);
351 add_cell(row, 'td', p.assists);
352 add_cell(row, 'td', p.hockey_assists);
353 add_cell(row, 'td', p.num_throws);
354 add_cell(row, 'td', p.throwaways);
355 add_cell(row, 'td', throw_ok.toFixed(0) + '%');
356 add_cell(row, 'td', p.catches);
357 add_cell(row, 'td', p.drops);
358 add_cell(row, 'td', catch_ok.toFixed(0) + '%');
359 add_cell(row, 'td', '+' + p.offensive_soft_plus);
360 add_cell(row, 'td', '-' + p.offensive_soft_minus);
366 function make_table_defense(players) {
369 let header = document.createElement('tr');
370 add_cell(header, 'th', 'Player');
371 add_cell(header, 'th', 'Ds');
372 add_cell(header, 'th', 'Pulls');
373 add_cell(header, 'th', 'OOB pulls');
374 add_cell(header, 'th', 'Avg. hang time (IB)');
375 add_cell(header, 'th', 'Soft +/-');
378 for (const [q,p] of Object.entries(players)) {
380 for (const t of p.pull_times) {
383 let avg_time = 1e-3 * sum_time / p.pulls;
384 let oob_pct = 100 * p.oob_pulls / p.pulls;
386 let row = document.createElement('tr');
387 add_cell(row, 'td', p.name); // TODO: number?
388 add_cell(row, 'td', p.defenses);
389 add_cell(row, 'td', p.pulls);
391 add_cell(row, 'td', 'N/A');
393 add_cell(row, 'td', p.oob_pulls + ' (' + oob_pct.toFixed(0) + '%)');
395 if (p.pulls > p.oob_pulls) {
396 add_cell(row, 'td', avg_time.toFixed(1) + ' sec');
398 add_cell(row, 'td', 'N/A');
400 add_cell(row, 'td', '+' + p.defensive_soft_plus);
401 add_cell(row, 'td', '-' + p.defensive_soft_minus);