PageSourceSearch

https://oseiskar.github.io/tree-of-life/search.js

js oseiskar.github.io collected 2026-10-03 09:44:09 UTC 6,374 bytes, 229 lines download raw bytes

1"use strict";
2
3/**
4 * An API to the prefix search tree that maps the names of the tree of life
5 * nodes to the node and subtree indices of those nodes.
6 * 
7 * Usage:
8 *      function resultCallback(query, resultTrieNode) { ... }
9 *      search_model.searchFor('Canis', resultCallback)
10 * 
11 * where resultTrieNode is like
12 * 
13 *     {
14 *       v: [
15 *         123,   // id of the node for the node called 'Canis lupus' 
16 *         2      // id of the subtree where that node is located
17 *       ],
18 *       c: {     // children 'Canis lupus' in the prefix tree
19 *          " familiaris": { ... },
20 *           ...
21 *       }
22 *     }
23 * 
24 * or null if the no name in the tree starts with the given query.
25 * The member v is set only if query corresponds to the exact name of
26 * some node in the tree. The member c can also be empty if query if is not
27 * a proper prefix of any other node in the tree.
28 * 
29 */
30function SearchModel(search_ready_callback) {
31    
32    var subtrees = {};
33    var root = {};
34    
35    var latest_query = '';
36    var request_counter = 0;
37    
38    var fetch = function (id, callback) {
39        if (subtrees[id]) {
40            callback(subtrees[id]);
41        }
42        else {
43            d3.json('data/search-'+id+'.json', function (error, data) {
44                if (error) return console.warn(error);
45                subtrees[id] = data;
46                callback(data);
47            });
48        }
49    };
50    
51    fetch(0, function (data) {
52        root = data;
53        if (search_ready_callback) search_ready_callback();
54    });
55    
56    function doSearch(tree, prefix, callback) {
57    
58        if (tree.subtree_index) {
59            fetch(tree.subtree_index, function (subtree) {
60                doSearch(subtree, prefix, callback);
61            });
62            return;
63        }
64        
65        if (prefix === '') {
66            callback(tree);
67            return;
68        }
69        
70        for (var key in tree.c) {
71            if (prefix.indexOf(key) == 0) {
72                doSearch(tree.c[key], prefix.substring(key.length), callback);
73                return;
74            }
75            if (key.indexOf(prefix) == 0) {
76                var new_tree = { c: {} };
77                new_tree.c[key.substring(prefix.length)] = tree.c[key];
78                callback(new_tree);
79                return;
80            }
81        }
82        callback(null);
83    };
84    
85    this.searchFor = function(query, callback) {
86        
87        if (latest_query === query) return;
88        latest_query = query;
89        
90        request_counter += 1;
91        doSearch(root, latest_query, function (result) {
92            request_counter -= 1;
93            if (latest_query === query) {
94                callback(query, result);
95            }
96        });
97    }
98    
99    this.requestPending = function() { return request_counter > 0; }
100}
101
102function SearchView(result_callback) {
103    var model = new SearchModel();
104    
105    var search_area = d3.select('#search');
106    var results_area = d3.select('#results');
107    var go_button = d3.select('#goto-link');
108    var go_button_action = null;
109    
110    this.clear = function () {
111        search_area.property('value', '');
112        go_button.classed('hidden', true);
113    }
114    
115    function openResult(search_result) {
116        result_callback(search_result[0], search_result[1]);
117    }
118
119    function capitalize(str) {
120        if (str == '') return '';
121        return str.charAt(0).toUpperCase() + str.slice(1);
122    }
123    
124    function checkLoader() {
125        d3.select('.loader-container')
126            .classed('loading-search', model.requestPending());
127    }
128    
129    function searchFor(query) {
130        model.searchFor(query, displayResult);
131        setTimeout(checkLoader, 200);
132    }
133
134    function displayResult(query, result) {
135        
136        checkLoader();
137        
138        var keys = [];
139        go_button_action = null;
140        
141        function onLineClick(l) {
142            var selected = result.c[l];
143            if (selected) {
144                search_area.property('value', query+l);
145                searchFor(query+l);
146                if (selected.v) openResult(selected.v);
147            }
148        }
149            
150        if (result === null) {
151            keys = [];
152            
153        } else {
154            keys = d3.keys(result.c);
155            
156            if (result.v) {
157                go_button_action = (function () {
158                    openResult(result.v);
159                    results_area.classed('hidden', true);
160                });
161            } else if (keys.length == 1) {
162                go_button_action = (function () {
163                    onLineClick(keys[0]);
164                });
165            } else if (query == '') {
166                go_button_action = (function () {
167                    result_callback(null);
168                });
169            }
170        }
171        
172        search_area.classed('not-found', result === null);
173        go_button.classed('hidden', go_button_action === null);
174        
175        results_area.classed('hidden', 
176            (query.length < 2 && keys.length > 10) ||
177            (keys.length == 1 && keys[0] == ''));
178        
179        var data = [];
180        
181        keys.sort();
182        for (var i in keys) {
183            var key = keys[i];
184            data.push(key);
185            if (i > 10) {
186                data.push('...');
187                break;
188            }
189        }
190        
191        var lines = results_area
192            .selectAll('a')
193            .data(data);
194        
195        lines.enter()
196            .append('a')
197            .attr('class', 'result-line')
198            .attr('href', 'javascript:void(0)');
199        
200        lines.on('click', function (l) { onLineClick(l); });
201            
202        lines.exit().remove();
203        
204        lines.text(function(v) {return capitalize(query+v)});
205    }
206    
207    go_button
208        .on('click', function () {
209            if (go_button_action !== null) {
210                go_button_action();
211                go_button.classed('hidden', true);
212            }
213        });
214
215    search_area.on('keyup', function () {
216        if (d3.event.keyCode === 13) { // Enter pressed
217            go_button.on('click')();
218        }
219        else {
220            searchFor(capitalize(this.value));
221        }
222    }).on('blur', function () {
223        setTimeout(function() {
224            results_area.classed('hidden', true);
225        }, 200);
226    });
227}
228
229

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.