X-Git-Url: https://git.sesse.net/?a=blobdiff_plain;f=ultimate.js;h=0c44658adcdd30a1b99ca2b45b696e5c2600aa12;hb=8b766d6cd0cb033a9929adc18eda0679cbd6ac39;hp=5c176e0deeb7309369e69b0c151942f0c7f4d000;hpb=133a9596bbe356ecda211eb95b8f9416bbe9a47e;p=pkanalytics diff --git a/ultimate.js b/ultimate.js index 5c176e0..0c44658 100644 --- a/ultimate.js +++ b/ultimate.js @@ -3,11 +3,13 @@ // No frameworks, no compilers, no npm, just JavaScript. :-) let global_json; +let global_filters = []; -addEventListener('hashchange', () => { process_matches(global_json); }); +addEventListener('hashchange', () => { process_matches(global_json, global_filters); }); +addEventListener('click', possibly_close_menu); fetch('ultimate.json') .then(response => response.json()) - .then(response => { global_json = response; process_matches(global_json); }); + .then(response => { global_json = response; process_matches(global_json, global_filters); }); function attribute_player_time(player, to, from, offense) { let delta_time; @@ -145,7 +147,7 @@ function add_3cell_ci(tr, ci) { element.appendChild(svg); } -function process_matches(json) { +function process_matches(json, filters) { let players = {}; for (const player of json['players']) { players[player['player_id']] = { @@ -206,6 +208,10 @@ function process_matches(json) { let globals = players['globals']; for (const match of json['matches']) { + if (!keep_match(match['match_id'], filters)) { + continue; + } + let our_score = 0; let their_score = 0; let handler = null; @@ -223,6 +229,8 @@ function process_matches(json) { p.last_point_seen = null; } for (const e of match['events']) { + // TODO: filter events + let t = e['t']; let type = e['type']; let p = players[e['player']]; @@ -902,3 +910,296 @@ function make_table_per_point(players) { return rows; } + +function open_filter_menu() { + document.getElementById('filter-submenu').style.display = 'none'; + + let menu = document.getElementById('filter-add-menu'); + menu.style.display = 'block'; + menu.replaceChildren(); + + // Place the menu directly under the “click to add” label; + // we don't anchor it since that label will move around + // and the menu shouldn't. + let rect = document.getElementById('filter-click-to-add').getBoundingClientRect(); + menu.style.left = rect.left + 'px'; + menu.style.top = (rect.bottom + 10) + 'px'; + + add_menu_item(menu, 0, 'match', 'Match (any)'); + add_menu_item(menu, 1, 'player_any', 'Player on field (any)'); + add_menu_item(menu, 2, 'player_all', 'Player on field (all)'); + // add_menu_item(menu, 'Formation played (any)'); +} + +function add_menu_item(menu, menu_idx, filter_type, title) { + let item = document.createElement('div'); + item.classList.add('option'); + item.appendChild(document.createTextNode(title)); + + let arrow = document.createElement('div'); + arrow.classList.add('arrow'); + arrow.textContent = '▸'; + item.appendChild(arrow); + + menu.appendChild(item); + + item.addEventListener('click', (e) => { show_submenu(menu_idx, null, filter_type); }); +} + +function show_submenu(menu_idx, pill, filter_type) { + let submenu = document.getElementById('filter-submenu'); + let subitems = []; + const filter = find_filter(filter_type); + + let choices = []; + if (filter_type === 'match') { + for (const match of global_json['matches']) { + choices.push({ + 'title': match['description'], + 'id': match['match_id'] + }); + } + } else if (filter_type === 'player_any' || filter_type === 'player_all') { + for (const player of global_json['players']) { + choices.push({ + 'title': player['name'], + 'id': player['player_id'] + }); + } + } + + for (const choice of choices) { + let label = document.createElement('label'); + + let subitem = document.createElement('div'); + subitem.classList.add('option'); + + let check = document.createElement('input'); + check.setAttribute('type', 'checkbox'); + check.setAttribute('id', 'choice' + choice.id); + if (filter !== null && filter.elements.has(choice.id)) { + check.setAttribute('checked', 'checked'); + } + check.addEventListener('change', (e) => { checkbox_changed(e, filter_type, choice.id); }); + + subitem.appendChild(check); + subitem.appendChild(document.createTextNode(choice.title)); + + label.appendChild(subitem); + subitems.push(label); + } + submenu.replaceChildren(...subitems); + submenu.style.display = 'block'; + + if (pill !== null) { + let rect = pill.getBoundingClientRect(); + submenu.style.top = (rect.bottom + 10) + 'px'; + submenu.style.left = rect.left + 'px'; + } else { + // Position just outside the selected menu. + let rect = document.getElementById('filter-add-menu').getBoundingClientRect(); + submenu.style.top = (rect.top + menu_idx * 35) + 'px'; + submenu.style.left = (rect.right - 1) + 'px'; + } +} + +// Find the right filter, if it exists. +function find_filter(filter_type) { + for (let f of global_filters) { + if (f.type === filter_type) { + return f; + } + } + return null; +} + +function checkbox_changed(e, filter_type, id) { + let filter = find_filter(filter_type); + if (e.target.checked) { + // See if we must add a new filter to the list. + if (filter === null) { + filter = { + 'type': filter_type, + 'elements': new Set([ id ]), + }; + filter.pill = make_filter_pill(filter); + global_filters.push(filter); + document.getElementById('filters').appendChild(filter.pill); + } else { + filter.elements.add(id); + let new_pill = make_filter_pill(filter); + document.getElementById('filters').replaceChild(new_pill, filter.pill); + filter.pill = new_pill; + } + } else { + filter.elements.delete(id); + if (filter.elements.size === 0) { + document.getElementById('filters').removeChild(filter.pill); + global_filters = global_filters.filter(f => f !== filter); + } else { + let new_pill = make_filter_pill(filter); + document.getElementById('filters').replaceChild(new_pill, filter.pill); + filter.pill = new_pill; + } + } + + process_matches(global_json, global_filters); +} + +function make_filter_pill(filter) { + let pill = document.createElement('div'); + pill.classList.add('filter-pill'); + let text; + if (filter.type === 'match') { + text = 'Match: '; + + // See if there's a common prefix. + let num_matches = filter.elements.size; + let common_prefix = null; + if (num_matches > 1) { + for (const match_id of filter.elements) { + let desc = find_match(match_id)['description']; + if (common_prefix === null) { + common_prefix = desc; + } else { + common_prefix = find_common_prefix(common_prefix, desc); + } + } + if (common_prefix.length < 3) { + common_prefix = null; + } + } + + if (common_prefix !== null) { + text += common_prefix + '('; + } + + let first = true; + let sorted_match_id = Array.from(filter.elements).sort((a, b) => a - b); + for (const match_id of sorted_match_id) { + if (!first) { + text += ', '; + } + let desc = find_match(match_id)['description']; + if (common_prefix === null) { + text += desc; + } else { + text += desc.substr(common_prefix.length); + } + first = false; + } + + if (common_prefix !== null) { + text += ')'; + } + } else if (filter.type === 'player_any') { + text = 'Player (any): '; + let sorted_players = Array.from(filter.elements).sort((a, b) => player_pos(a) - player_pos(b)); + let first = true; + for (const player_id of sorted_players) { + if (!first) { + text += ', '; + } + text += find_player(player_id)['name']; + first = false; + } + } else if (filter.type === 'player_all') { + text = 'Players: '; + let sorted_players = Array.from(filter.elements).sort((a, b) => player_pos(a) - player_pos(b)); + let first = true; + for (const player_id of sorted_players) { + if (!first) { + text += ' AND '; + } + text += find_player(player_id)['name']; + first = false; + } + } + + let text_node = document.createElement('span'); + text_node.innerText = text; + text_node.addEventListener('click', (e) => show_submenu(null, pill, filter.type)); + pill.appendChild(text_node); + + pill.appendChild(document.createTextNode(' ')); + + let delete_node = document.createElement('span'); + delete_node.innerText = '✖'; + delete_node.addEventListener('click', (e) => { + // Delete this filter entirely. + document.getElementById('filters').removeChild(pill); + global_filters = global_filters.filter(f => f !== filter); + process_matches(global_json, global_filters); + + let add_menu = document.getElementById('filter-add-menu'); + let add_submenu = document.getElementById('filter-submenu'); + add_menu.style.display = 'none'; + add_submenu.style.display = 'none'; + }); + pill.appendChild(delete_node); + pill.style.cursor = 'pointer'; + + return pill; +} + +function find_common_prefix(a, b) { + let ret = ''; + for (let i = 0; i < Math.min(a.length, b.length); ++i) { + if (a[i] === b[i]) { + ret += a[i]; + } else { + break; + } + } + return ret; +} + +function find_match(match_id) { + for (const match of global_json['matches']) { + if (match['match_id'] === match_id) { + return match; + } + } + return null; +} + +function find_player(player_id) { + for (const player of global_json['players']) { + if (player['player_id'] === player_id) { + return player; + } + } + return null; +} + +function player_pos(player_id) { + let i = 0; + for (const player of global_json['players']) { + if (player['player_id'] === player_id) { + return i; + } + ++i; + } + return null; +} + +function keep_match(match_id, filters) { + for (const filter of filters) { + if (filter.type === 'match') { + return filter.elements.has(match_id); + } + } + return true; +} + +function possibly_close_menu(e) { + if (e.target.closest('#filter-click-to-add') === null && + e.target.closest('#filter-add-menu') === null && + e.target.closest('#filter-submenu') === null && + e.target.closest('.filter-pill') === null) { + let add_menu = document.getElementById('filter-add-menu'); + let add_submenu = document.getElementById('filter-submenu'); + add_menu.style.display = 'none'; + add_submenu.style.display = 'none'; + } +}