]> git.sesse.net Git - pkanalytics/blobdiff - ultimate.js
Begin supporting filters.
[pkanalytics] / ultimate.js
index 5c176e0deeb7309369e69b0c151942f0c7f4d000..0c44658adcdd30a1b99ca2b45b696e5c2600aa12 100644 (file)
@@ -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';
+       }
+}