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,
82 for (const match of json['matches']) {
84 let prev_handler = null;
85 let live_since = null;
88 let pull_started = null;
89 let last_pull_was_ours = null; // Effectively whether we're playing an O or D point (not affected by turnovers).
92 for (const [q,p] of Object.entries(players)) {
93 p.on_field_since = null;
94 p.last_point_seen = null;
96 for (const e of match['events']) {
99 let p = players[e['player']];
102 if (type === 'in' && p.on_field_since === null) {
103 p.on_field_since = t;
104 } else if (type === 'out') {
105 take_off_field(p, t, live_since);
108 // Liveness management
109 if (type === 'pull' || type === 'their_pull' || type === 'restart') {
111 } else if (type === 'goal' || type === 'their_goal' || type === 'stoppage') {
112 for (const [q,p] of Object.entries(players)) {
113 if (p.on_field_since === null) {
116 if (p.last_point_seen !== point_num) {
117 // In case the player did nothing this point,
118 // not even subbing in.
119 p.last_point_seen = point_num;
122 attribute_player_time(p, t, live_since);
124 if (last_pull_was_ours === true) { // D point.
125 ++p.defensive_points_completed;
126 if (type === 'goal') {
127 ++p.defensive_points_won;
129 } else if (last_pull_was_ours === false) { // O point.
130 ++p.offensive_points_completed;
131 if (type === 'goal') {
132 ++p.offensive_points_won;
139 // Point count management
140 if (p !== undefined && type !== 'out' && p.last_point_seen !== point_num) {
141 p.last_point_seen = point_num;
144 if (type === 'goal' || type === 'their_goal') {
150 if (type === 'pull') {
151 puller = e['player'];
154 } else if (type === 'in' || type === 'out' || type === 'stoppage' || type === 'restart' || type === 'unknown' || type === 'set_defense' || type === 'set_offense') {
155 // No effect on pull.
156 } else if (type === 'pull_landed' && puller !== null) {
157 players[puller].pull_times.push(t - pull_started);
158 } else if (type === 'pull_oob' && puller !== null) {
159 ++players[puller].oob_pulls;
161 // Not pulling (if there was one, we never recorded its outcome, but still count it).
162 puller = pull_started = null;
165 // Offense/defense management (TODO: use it for actual counting)
166 if (type === 'set_defense' || type === 'goal' || type === 'throwaway' || type === 'drop') {
168 } else if (type === 'set_offense' || type === 'their_goal' || type === 'their_throwaway' || type === 'defense' || type === 'interception') {
171 if (type === 'pull') {
172 last_pull_was_ours = true;
173 } else if (type === 'their_pull') {
174 last_pull_was_ours = false;
175 } else if (type === 'set_offense' && last_pull_was_ours === null) {
176 // set_offense could either be “changed to offense for some reason
177 // we could not express”, or “we started in the middle of a point,
178 // and we are offense”. We assume that if we already saw the pull,
179 // it's the former, and if not, it's the latter; thus, the === null
180 // test above. (It could also be “we set offense before the pull,
181 // so that we get the right button enabled”, in which case it will
182 // be overwritten by the next pull/their_pull event anyway.)
183 last_pull_was_ours = false;
184 } else if (type === 'set_defense' && last_pull_was_ours === null) {
186 last_pull_was_ours = true;
187 } else if (type === 'goal' || type === 'their_goal') {
188 last_pull_was_ours = null;
192 if (type === 'catch' || type === 'goal') {
193 if (handler !== null) {
194 ++players[handler].num_throws;
199 if (type === 'goal') {
200 if (prev_handler !== null) {
201 ++players[prev_handler].hockey_assists;
203 if (handler !== null) {
204 ++players[handler].assists;
207 handler = prev_handler = null;
209 // Update hold history.
210 prev_handler = handler;
211 handler = e['player'];
213 } else if (type === 'throwaway') {
216 handler = prev_handler = null;
217 } else if (type === 'drop') {
219 handler = prev_handler = null;
220 } else if (type === 'defense') {
222 } else if (type === 'interception') {
227 handler = e['player'];
228 } else if (type === 'offensive_soft_plus' || type === 'offensive_soft_minus' || type === 'defensive_soft_plus' || type === 'defensive_soft_minus') {
230 } else if (type !== 'in' && type !== 'out' && type !== 'pull' &&
231 type !== 'their_goal' && type !== 'stoppage' && type !== 'unknown' &&
232 type !== 'set_defense' && type !== 'goal' && type !== 'throwaway' &&
233 type !== 'drop' && type !== 'set_offense' && type !== 'their_goal' &&
234 type !== 'pull' && type !== 'pull_landed' && type !== 'pull_oob' &&
235 type !== 'their_throwaway' && type !== 'defense' && type !== 'interception') {
236 console.log("Unknown event:", e);
240 // Add field time for all players still left at match end.
241 for (const [q,p] of Object.entries(players)) {
242 if (p.on_field_since !== null && last_goal !== null) {
243 p.field_time_ms += last_goal - p.on_field_since;
248 let chosen_category = get_chosen_category();
249 write_main_menu(chosen_category);
252 if (chosen_category === 'general') {
253 rows = make_table_general(players);
254 } else if (chosen_category === 'offense') {
255 rows = make_table_offense(players);
256 } else if (chosen_category === 'defense') {
257 rows = make_table_defense(players);
258 } else if (chosen_category === 'playing_time') {
259 rows = make_table_playing_time(players);
261 document.getElementById('stats').replaceChildren(...rows);
264 function get_chosen_category() {
265 if (window.location.hash === '#offense') {
267 } else if (window.location.hash === '#defense') {
269 } else if (window.location.hash === '#playing_time') {
270 return 'playing_time';
276 function write_main_menu(chosen_category) {
278 if (chosen_category === 'general') {
279 elems.push(document.createTextNode('General'));
281 let a = document.createElement('a');
282 a.appendChild(document.createTextNode('General'));
283 a.setAttribute('href', '#general');
287 elems.push(document.createTextNode(' | '));
288 if (chosen_category === 'offense') {
289 elems.push(document.createTextNode('Offense'));
291 let a = document.createElement('a');
292 a.appendChild(document.createTextNode('Offense'));
293 a.setAttribute('href', '#offense');
297 elems.push(document.createTextNode(' | '));
298 if (chosen_category === 'defense') {
299 elems.push(document.createTextNode('Defense'));
301 let a = document.createElement('a');
302 a.appendChild(document.createTextNode('Defense'));
303 a.setAttribute('href', '#defense');
307 elems.push(document.createTextNode(' | '));
308 if (chosen_category === 'playing_time') {
309 elems.push(document.createTextNode('Playing time'));
311 let a = document.createElement('a');
312 a.appendChild(document.createTextNode('Playing time'));
313 a.setAttribute('href', '#playing_time');
317 document.getElementById('mainmenu').replaceChildren(...elems);
320 function make_table_general(players) {
323 let header = document.createElement('tr');
324 add_cell(header, 'th', 'Player');
325 add_cell(header, 'th', '+/-');
326 add_cell(header, 'th', 'Soft +/-');
327 add_cell(header, 'th', 'O efficiency');
328 add_cell(header, 'th', 'D efficiency');
329 add_cell(header, 'th', 'Points played');
333 for (const [q,p] of Object.entries(players)) {
334 let row = document.createElement('tr');
335 let pm = p.goals + p.assists + p.hockey_assists + p.defenses - p.throwaways - p.drops;
336 let soft_pm = p.offensive_soft_plus + p.defensive_soft_plus - p.offensive_soft_minus - p.defensive_soft_minus;
337 let o_efficiency = (p.offensive_points_won / p.offensive_points_completed) * 2 - 1;
338 let d_efficiency = (p.defensive_points_won / p.defensive_points_completed) * 2 - 1;
339 add_cell(row, 'td', p.name); // TODO: number?
340 add_cell(row, 'td', pm > 0 ? ('+' + pm) : pm);
341 add_cell(row, 'td', soft_pm > 0 ? ('+' + soft_pm) : soft_pm);
342 add_cell(row, 'td', p.offensive_points_completed > 0 ? o_efficiency.toFixed(2) : 'N/A');
343 add_cell(row, 'td', p.defensive_points_completed > 0 ? d_efficiency.toFixed(2) : 'N/A');
344 add_cell(row, 'td', p.points_played);
350 function make_table_offense(players) {
353 let header = document.createElement('tr');
354 add_cell(header, 'th', 'Player');
355 add_cell(header, 'th', 'Goals');
356 add_cell(header, 'th', 'Assists');
357 add_cell(header, 'th', 'Hockey assists');
358 add_cell(header, 'th', 'Throws');
359 add_cell(header, 'th', 'Throwaways');
360 add_cell(header, 'th', '%OK');
361 add_cell(header, 'th', 'Catches');
362 add_cell(header, 'th', 'Drops');
363 add_cell(header, 'th', '%OK');
364 add_cell(header, 'th', 'Soft +/-');
368 for (const [q,p] of Object.entries(players)) {
369 let throw_ok = 100 * (1 - p.throwaways / p.num_throws);
370 let catch_ok = 100 * (p.catches / (p.catches + p.drops));
372 let row = document.createElement('tr');
373 add_cell(row, 'td', p.name); // TODO: number?
374 add_cell(row, 'td', p.goals);
375 add_cell(row, 'td', p.assists);
376 add_cell(row, 'td', p.hockey_assists);
377 add_cell(row, 'td', p.num_throws);
378 add_cell(row, 'td', p.throwaways);
379 add_cell(row, 'td', throw_ok.toFixed(0) + '%');
380 add_cell(row, 'td', p.catches);
381 add_cell(row, 'td', p.drops);
382 add_cell(row, 'td', catch_ok.toFixed(0) + '%');
383 add_cell(row, 'td', '+' + p.offensive_soft_plus);
384 add_cell(row, 'td', '-' + p.offensive_soft_minus);
390 function make_table_defense(players) {
393 let header = document.createElement('tr');
394 add_cell(header, 'th', 'Player');
395 add_cell(header, 'th', 'Ds');
396 add_cell(header, 'th', 'Pulls');
397 add_cell(header, 'th', 'OOB pulls');
398 add_cell(header, 'th', 'Avg. hang time (IB)');
399 add_cell(header, 'th', 'Soft +/-');
402 for (const [q,p] of Object.entries(players)) {
404 for (const t of p.pull_times) {
407 let avg_time = 1e-3 * sum_time / p.pulls;
408 let oob_pct = 100 * p.oob_pulls / p.pulls;
410 let row = document.createElement('tr');
411 add_cell(row, 'td', p.name); // TODO: number?
412 add_cell(row, 'td', p.defenses);
413 add_cell(row, 'td', p.pulls);
415 add_cell(row, 'td', 'N/A');
417 add_cell(row, 'td', p.oob_pulls + ' (' + oob_pct.toFixed(0) + '%)');
419 if (p.pulls > p.oob_pulls) {
420 add_cell(row, 'td', avg_time.toFixed(1) + ' sec');
422 add_cell(row, 'td', 'N/A');
424 add_cell(row, 'td', '+' + p.defensive_soft_plus);
425 add_cell(row, 'td', '-' + p.defensive_soft_minus);
431 function make_table_playing_time(players) {
434 let header = document.createElement('tr');
435 add_cell(header, 'th', 'Player');
436 add_cell(header, 'th', 'Points played');
437 add_cell(header, 'th', 'Time played');
438 add_cell(header, 'th', 'Time on field');
439 add_cell(header, 'th', 'O points');
440 add_cell(header, 'th', 'D points');
444 for (const [q,p] of Object.entries(players)) {
445 let row = document.createElement('tr');
446 add_cell(row, 'td', p.name); // TODO: number?
447 add_cell(row, 'td', p.points_played);
448 add_cell(row, 'td', Math.floor(p.playing_time_ms / 60000) + ' min');
449 add_cell(row, 'td', Math.floor(p.field_time_ms / 60000) + ' min');
450 add_cell(row, 'td', p.offensive_points_completed);
451 add_cell(row, 'td', p.defensive_points_completed);