3 // No frameworks, no compilers, no npm, just JavaScript. :-)
7 addEventListener('hashchange', () => { 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 if (player.on_field_since !== null) { // Just a safeguard; out without in should never happen.
28 player.field_time_ms += t - player.on_field_since;
30 player.on_field_since = null;
33 function add_cell(tr, element_type, text) {
34 let element = document.createElement(element_type);
35 element.textContent = text;
36 tr.appendChild(element);
39 function process_matches(json) {
41 for (const player of json['players']) {
42 players[player['player_id']] = {
43 'name': player['name'],
44 'number': player['number'],
62 'offensive_points_completed': 0,
63 'offensive_points_won': 0,
64 'defensive_points_completed': 0,
65 'defensive_points_won': 0,
67 'offensive_soft_plus': 0,
68 'offensive_soft_minus': 0,
69 'defensive_soft_plus': 0,
70 'defensive_soft_minus': 0,
77 'last_point_seen': null,
78 'on_field_since': null,
83 players['globals'] = {
88 'offensive_points_completed': 0,
89 'offensive_points_won': 0,
90 'defensive_points_completed': 0,
91 'defensive_points_won': 0,
93 let globals = players['globals'];
95 for (const match of json['matches']) {
97 let prev_handler = null;
98 let live_since = null;
101 let pull_started = null;
102 let last_pull_was_ours = null; // Effectively whether we're playing an O or D point (not affected by turnovers).
104 let game_started = null;
105 let last_goal = null;
106 for (const [q,p] of Object.entries(players)) {
107 p.on_field_since = null;
108 p.last_point_seen = null;
110 for (const e of match['events']) {
112 let type = e['type'];
113 let p = players[e['player']];
116 if (type === 'in' && p.on_field_since === null) {
117 p.on_field_since = t;
118 } else if (type === 'out') {
119 take_off_field(p, t, live_since);
122 // Liveness management
123 if (type === 'pull' || type === 'their_pull' || type === 'restart') {
125 } else if (type === 'goal' || type === 'their_goal' || type === 'stoppage') {
126 for (const [q,p] of Object.entries(players)) {
127 if (p.on_field_since === null) {
130 if (p.last_point_seen !== point_num) {
131 // In case the player did nothing this point,
132 // not even subbing in.
133 p.last_point_seen = point_num;
136 attribute_player_time(p, t, live_since);
138 if (last_pull_was_ours === true) { // D point.
139 ++p.defensive_points_completed;
140 if (type === 'goal') {
141 ++p.defensive_points_won;
143 } else if (last_pull_was_ours === false) { // O point.
144 ++p.offensive_points_completed;
145 if (type === 'goal') {
146 ++p.offensive_points_won;
152 ++globals.points_played;
153 if (live_since !== null) {
154 globals.playing_time_ms += t - live_since;
156 if (last_pull_was_ours === true) { // D point.
157 ++globals.defensive_points_completed;
158 if (type === 'goal') {
159 ++globals.defensive_points_won;
161 } else if (last_pull_was_ours === false) { // O point.
162 ++globals.offensive_points_completed;
163 if (type === 'goal') {
164 ++globals.offensive_points_won;
170 // Point count management
171 if (p !== undefined && type !== 'out' && p.last_point_seen !== point_num) {
172 p.last_point_seen = point_num;
175 if (type === 'goal' || type === 'their_goal') {
179 if (type !== 'out' && game_started === null) {
184 if (type === 'pull') {
185 puller = e['player'];
188 } else if (type === 'in' || type === 'out' || type === 'stoppage' || type === 'restart' || type === 'unknown' || type === 'set_defense' || type === 'set_offense') {
189 // No effect on pull.
190 } else if (type === 'pull_landed' && puller !== null) {
191 players[puller].pull_times.push(t - pull_started);
192 } else if (type === 'pull_oob' && puller !== null) {
193 ++players[puller].oob_pulls;
195 // Not pulling (if there was one, we never recorded its outcome, but still count it).
196 puller = pull_started = null;
199 // Offense/defense management (TODO: use it for actual counting)
200 if (type === 'set_defense' || type === 'goal' || type === 'throwaway' || type === 'drop') {
202 } else if (type === 'set_offense' || type === 'their_goal' || type === 'their_throwaway' || type === 'defense' || type === 'interception') {
205 if (type === 'pull') {
206 last_pull_was_ours = true;
207 } else if (type === 'their_pull') {
208 last_pull_was_ours = false;
209 } else if (type === 'set_offense' && last_pull_was_ours === null) {
210 // set_offense could either be “changed to offense for some reason
211 // we could not express”, or “we started in the middle of a point,
212 // and we are offense”. We assume that if we already saw the pull,
213 // it's the former, and if not, it's the latter; thus, the === null
214 // test above. (It could also be “we set offense before the pull,
215 // so that we get the right button enabled”, in which case it will
216 // be overwritten by the next pull/their_pull event anyway.)
217 last_pull_was_ours = false;
218 } else if (type === 'set_defense' && last_pull_was_ours === null) {
220 last_pull_was_ours = true;
221 } else if (type === 'goal' || type === 'their_goal') {
222 last_pull_was_ours = null;
226 if (type === 'catch' || type === 'goal') {
227 if (handler !== null) {
228 ++players[handler].num_throws;
233 if (type === 'goal') {
234 if (prev_handler !== null) {
235 ++players[prev_handler].hockey_assists;
237 if (handler !== null) {
238 ++players[handler].assists;
241 handler = prev_handler = null;
243 // Update hold history.
244 prev_handler = handler;
245 handler = e['player'];
247 } else if (type === 'throwaway') {
250 handler = prev_handler = null;
251 } else if (type === 'drop') {
253 handler = prev_handler = null;
254 } else if (type === 'defense') {
256 } else if (type === 'interception') {
261 handler = e['player'];
262 } else if (type === 'offensive_soft_plus' || type === 'offensive_soft_minus' || type === 'defensive_soft_plus' || type === 'defensive_soft_minus') {
264 } else if (type !== 'in' && type !== 'out' && type !== 'pull' &&
265 type !== 'their_goal' && type !== 'stoppage' && type !== 'unknown' &&
266 type !== 'set_defense' && type !== 'goal' && type !== 'throwaway' &&
267 type !== 'drop' && type !== 'set_offense' && type !== 'their_goal' &&
268 type !== 'pull' && type !== 'pull_landed' && type !== 'pull_oob' &&
269 type !== 'their_throwaway' && type !== 'defense' && type !== 'interception') {
270 console.log("Unknown event:", e);
274 // Add field time for all players still left at match end.
275 for (const [q,p] of Object.entries(players)) {
276 if (p.on_field_since !== null && last_goal !== null) {
277 p.field_time_ms += last_goal - p.on_field_since;
280 if (game_started !== null && last_goal !== null) {
281 globals.field_time_ms += last_goal - game_started;
283 if (live_since !== null && last_goal !== null) {
284 globals.playing_time_ms += last_goal - live_since;
288 let chosen_category = get_chosen_category();
289 write_main_menu(chosen_category);
292 if (chosen_category === 'general') {
293 rows = make_table_general(players);
294 } else if (chosen_category === 'offense') {
295 rows = make_table_offense(players);
296 } else if (chosen_category === 'defense') {
297 rows = make_table_defense(players);
298 } else if (chosen_category === 'playing_time') {
299 rows = make_table_playing_time(players);
301 document.getElementById('stats').replaceChildren(...rows);
304 function get_chosen_category() {
305 if (window.location.hash === '#offense') {
307 } else if (window.location.hash === '#defense') {
309 } else if (window.location.hash === '#playing_time') {
310 return 'playing_time';
316 function write_main_menu(chosen_category) {
318 if (chosen_category === 'general') {
319 elems.push(document.createTextNode('General'));
321 let a = document.createElement('a');
322 a.appendChild(document.createTextNode('General'));
323 a.setAttribute('href', '#general');
327 elems.push(document.createTextNode(' | '));
328 if (chosen_category === 'offense') {
329 elems.push(document.createTextNode('Offense'));
331 let a = document.createElement('a');
332 a.appendChild(document.createTextNode('Offense'));
333 a.setAttribute('href', '#offense');
337 elems.push(document.createTextNode(' | '));
338 if (chosen_category === 'defense') {
339 elems.push(document.createTextNode('Defense'));
341 let a = document.createElement('a');
342 a.appendChild(document.createTextNode('Defense'));
343 a.setAttribute('href', '#defense');
347 elems.push(document.createTextNode(' | '));
348 if (chosen_category === 'playing_time') {
349 elems.push(document.createTextNode('Playing time'));
351 let a = document.createElement('a');
352 a.appendChild(document.createTextNode('Playing time'));
353 a.setAttribute('href', '#playing_time');
357 document.getElementById('mainmenu').replaceChildren(...elems);
360 function make_table_general(players) {
363 let header = document.createElement('tr');
364 add_cell(header, 'th', 'Player');
365 add_cell(header, 'th', '+/-');
366 add_cell(header, 'th', 'Soft +/-');
367 add_cell(header, 'th', 'O efficiency');
368 add_cell(header, 'th', 'D efficiency');
369 add_cell(header, 'th', 'Points played');
373 for (const [q,p] of Object.entries(players)) {
374 if (q === 'globals') continue;
375 let row = document.createElement('tr');
376 let pm = p.goals + p.assists + p.hockey_assists + p.defenses - p.throwaways - p.drops;
377 let soft_pm = p.offensive_soft_plus + p.defensive_soft_plus - p.offensive_soft_minus - p.defensive_soft_minus;
378 let o_efficiency = (p.offensive_points_won / p.offensive_points_completed) * 2 - 1;
379 let d_efficiency = (p.defensive_points_won / p.defensive_points_completed) * 2 - 1;
380 add_cell(row, 'td', p.name); // TODO: number?
381 add_cell(row, 'td', pm > 0 ? ('+' + pm) : pm);
382 add_cell(row, 'td', soft_pm > 0 ? ('+' + soft_pm) : soft_pm);
383 add_cell(row, 'td', p.offensive_points_completed > 0 ? o_efficiency.toFixed(2) : 'N/A');
384 add_cell(row, 'td', p.defensive_points_completed > 0 ? d_efficiency.toFixed(2) : 'N/A');
385 add_cell(row, 'td', p.points_played);
390 let globals = players['globals'];
391 let o_efficiency = (globals.offensive_points_won / globals.offensive_points_completed) * 2 - 1;
392 let d_efficiency = (globals.defensive_points_won / globals.defensive_points_completed) * 2 - 1;
393 let row = document.createElement('tr');
394 add_cell(row, 'td', '');
395 add_cell(row, 'td', '');
396 add_cell(row, 'td', '');
397 add_cell(row, 'td', globals.offensive_points_completed > 0 ? o_efficiency.toFixed(2) : 'N/A');
398 add_cell(row, 'td', globals.defensive_points_completed > 0 ? d_efficiency.toFixed(2) : 'N/A');
399 add_cell(row, 'td', globals.points_played);
405 function make_table_offense(players) {
408 let header = document.createElement('tr');
409 add_cell(header, 'th', 'Player');
410 add_cell(header, 'th', 'Goals');
411 add_cell(header, 'th', 'Assists');
412 add_cell(header, 'th', 'Hockey assists');
413 add_cell(header, 'th', 'Throws');
414 add_cell(header, 'th', 'Throwaways');
415 add_cell(header, 'th', '%OK');
416 add_cell(header, 'th', 'Catches');
417 add_cell(header, 'th', 'Drops');
418 add_cell(header, 'th', '%OK');
419 add_cell(header, 'th', 'Soft +/-');
427 for (const [q,p] of Object.entries(players)) {
428 if (q === 'globals') continue;
429 let throw_ok = 100 * (1 - p.throwaways / p.num_throws);
430 let catch_ok = 100 * (p.catches / (p.catches + p.drops));
432 let row = document.createElement('tr');
433 add_cell(row, 'td', p.name); // TODO: number?
434 add_cell(row, 'td', p.goals);
435 add_cell(row, 'td', p.assists);
436 add_cell(row, 'td', p.hockey_assists);
437 add_cell(row, 'td', p.num_throws);
438 add_cell(row, 'td', p.throwaways);
439 add_cell(row, 'td', throw_ok.toFixed(0) + '%');
440 add_cell(row, 'td', p.catches);
441 add_cell(row, 'td', p.drops);
442 add_cell(row, 'td', catch_ok.toFixed(0) + '%');
443 add_cell(row, 'td', '+' + p.offensive_soft_plus);
444 add_cell(row, 'td', '-' + p.offensive_soft_minus);
447 num_throws += p.num_throws;
448 throwaways += p.throwaways;
449 catches += p.catches;
454 let throw_ok = 100 * (1 - throwaways / num_throws);
455 let catch_ok = 100 * (catches / (catches + drops));
457 let row = document.createElement('tr');
458 add_cell(row, 'td', '');
459 add_cell(row, 'td', '');
460 add_cell(row, 'td', '');
461 add_cell(row, 'td', '');
462 add_cell(row, 'td', num_throws);
463 add_cell(row, 'td', throwaways);
464 add_cell(row, 'td', throw_ok.toFixed(0) + '%');
465 add_cell(row, 'td', catches);
466 add_cell(row, 'td', drops);
467 add_cell(row, 'td', catch_ok.toFixed(0) + '%');
468 add_cell(row, 'td', '');
469 add_cell(row, 'td', '');
475 function make_table_defense(players) {
478 let header = document.createElement('tr');
479 add_cell(header, 'th', 'Player');
480 add_cell(header, 'th', 'Ds');
481 add_cell(header, 'th', 'Pulls');
482 add_cell(header, 'th', 'OOB pulls');
483 add_cell(header, 'th', 'Avg. hang time (IB)');
484 add_cell(header, 'th', 'Soft +/-');
487 for (const [q,p] of Object.entries(players)) {
488 if (q === 'globals') continue;
490 for (const t of p.pull_times) {
493 let avg_time = 1e-3 * sum_time / p.pulls;
494 let oob_pct = 100 * p.oob_pulls / p.pulls;
496 let row = document.createElement('tr');
497 add_cell(row, 'td', p.name); // TODO: number?
498 add_cell(row, 'td', p.defenses);
499 add_cell(row, 'td', p.pulls);
501 add_cell(row, 'td', 'N/A');
503 add_cell(row, 'td', p.oob_pulls + ' (' + oob_pct.toFixed(0) + '%)');
505 if (p.pulls > p.oob_pulls) {
506 add_cell(row, 'td', avg_time.toFixed(1) + ' sec');
508 add_cell(row, 'td', 'N/A');
510 add_cell(row, 'td', '+' + p.defensive_soft_plus);
511 add_cell(row, 'td', '-' + p.defensive_soft_minus);
517 function make_table_playing_time(players) {
520 let header = document.createElement('tr');
521 add_cell(header, 'th', 'Player');
522 add_cell(header, 'th', 'Points played');
523 add_cell(header, 'th', 'Time played');
524 add_cell(header, 'th', 'Time on field');
525 add_cell(header, 'th', 'O points');
526 add_cell(header, 'th', 'D points');
530 for (const [q,p] of Object.entries(players)) {
531 if (q === 'globals') continue;
532 let row = document.createElement('tr');
533 add_cell(row, 'td', p.name); // TODO: number?
534 add_cell(row, 'td', p.points_played);
535 add_cell(row, 'td', Math.floor(p.playing_time_ms / 60000) + ' min');
536 add_cell(row, 'td', Math.floor(p.field_time_ms / 60000) + ' min');
537 add_cell(row, 'td', p.offensive_points_completed);
538 add_cell(row, 'td', p.defensive_points_completed);
543 let globals = players['globals'];
544 let row = document.createElement('tr');
545 add_cell(row, 'td', '');
546 add_cell(row, 'td', globals.points_played);
547 add_cell(row, 'td', Math.floor(globals.playing_time_ms / 60000) + ' min');
548 add_cell(row, 'td', Math.floor(globals.field_time_ms / 60000) + ' min');
549 add_cell(row, 'td', globals.offensive_points_completed);
550 add_cell(row, 'td', globals.defensive_points_completed);