1 /* (This is the new BSD license.)
  2 * Copyright (c) 2014-2016, Chris Culy
  3 * All rights reserved.
  4 *
  5 * Redistribution and use in source and binary forms, with or without
  6 * modification, are permitted provided that the following conditions are met:
  7 *     * Redistributions of source code must retain the above copyright
  8 *       notice, this list of conditions and the following disclaimer.
  9 *     * Redistributions in binary form must reproduce the above copyright
 10 *       notice, this list of conditions and the following disclaimer in the
 11 *       documentation and/or other materials provided with the distribution.
 12 *     * Neither the name of the Chris Culy nor the 
 13 *		names of its contributors may be used to endorse or promote 
 14 *		products from this software without specific prior written permission.
 15 *
 16 * THIS SOFTWARE IS PROVIDED BY Chris Culy
 17 * ``AS IS'' AND ANY OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
 18 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 19 * ARE DISCLAIMED. IN NO EVENT SHALL Chris Culy
 20 * BE LIABLE FOR ANY, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 21 * CONSEQUENTIAL DAMAGES INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 
 22 * GOODS OR SERVICES; OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 23 * CAUSED AND ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 
 24 * TORT INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 
 25 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 26 */
 27 
 28 "use strict";
 29 /**
 30  @namespace kwicIS
 31  All of the functionality is in the kwicIS namespace
 32 */
 33 var kwicIS = kwicIS || {};
 34 
 35 (function(){
 36 
 37 //the container is either an ID or the HTMl element
 38 /**
 39   * @class kwicIS.KWIC
 40   *  This is the class for the KWIC
 41   * <p>
 42   * The constructor
 43   * @param container is the html element that will contain the KWIC. An id can be used instead.
 44   */
 45 kwicIS.KWIC = function(container) {
 46     var where; //the html element for the KWIC
 47     var id; //the id of the element; we'll make up one if we need to
 48     var data; // array of objects: {left: array of (TextInfo) leftContexts, hit:(TextInfo), right: array of (TextInfo) rightContexts, index:idx]}
 49     var fromObjs = false; //is the input data objects (e.g. TextInfo), or delimited Strings
 50     //if we have delimited strings as input, we need these
 51     var fldNames = ["token"];
 52     var fldDelim = "/";
 53     var textInfoToString; //toString function for textInfo
 54     var tmu = textmodel.TextModelUtilities(fldNames, fldDelim, textInfoToString); //for when we get delimited string input
 55     var filters = {}; // {left: array of filter_functions, hit:filter_function, right: array of filter_functions. a filter_function gets an info object and returns Boolean. The function at index <em>i</em> filters position <em>i + 1</em> away from the root. Default is no filtering (via empty arrays)
 56     var excludedIDs = d3.set(); //ids excluded by filters
 57     //
 58     var numHits;
 59     var leftContextLen = 5; 
 60     var rtContextLen = 5;
 61     var numToShow = 20; //number of hits to show at one time
 62     var whichPage = 0; //which "page" of hits we are showing
 63     var lastPage; //the last page; read only
 64     var sortCol = 0; //i.e. the hit. For now just one column.
 65     var headerData;
 66     var headers;
 67     var cellTitle = function(textInfo, i) {}; //noop
 68     var colHeaderClick = function(headerContent, col) {}; //noop
 69     var itemCellClick = function(textInfo, i) {}; //noop
 70     var rowString = function(textInfoArray, i) {}; //noop
 71     var rowStringOnRight;
 72     var haveRowString = false;
 73     var prefixesOnRight = false;
 74     
 75     
 76     var kwic = this; //a way to keep track of the main object since functions each have their own _this_
 77     
 78     //we can pass either an ID or the actual HTML element this way
 79     if (typeof(container) == "string") {
 80         where = document.getElementById(container);
 81         id = container;
 82     } else {
 83         where = container; // not ideal -- should check that this is really html element
 84         id = container.id;
 85         if (!id) {
 86             container.id = "kwikContainer33";
 87             id = container.id;
 88         }
 89     }
 90     
 91     var table = d3.select(where).append("table").attr("class", "kwicTable");
 92     table.append("thead").append("tr");
 93     table.append("tbody");
 94     
 95     setupHeaderData();
 96     
 97     //end of initialization in constructor
 98     
 99     
100     ///////////     public functions
101     
102     
103     // setter/getters
104     
105     /**
106      * Getter/setter for the data for the KWIC
107      * @param input is the data. It can be <strong>either</strong>: 
108      * <p>
109      * an array of of arrays [leftContextsArray, hitsArray, rightContextsArray, indexArray], where the i-th elements of the arrays correspond
110      * and the contexts and hits are delimited strings (as defined by {@link kwicIS.KWIC#fieldNames} and {@link kwicIS.KWIC#fieldDelim}
111      * <p>
112      * <strong>or</strong>
113      * <p>
114      * an array of objects, where each object is of the form: {left: array of leftContexts, hits:element, right: array of rightContexts, index:element},
115      * and all the elements of the contexts and the hits are themselves objects, typically with at least a "token" key.
116      * <p>
117      * If the input is delimited strings, they will be converted internally to #textmodel.TextInfo objects
118      * <p>
119      * If not specified, the current data is returned
120      */
121     this.data = function(input) {
122         if (!arguments.length) {
123             return data;
124         }
125         
126         //autodetect whether the input is strings or objects
127         if (typeof(input[0]) === "object" && input[0] instanceof Array) { //cf. http://javascript.crockford.com/remedial.html
128             data = [];
129             numHits = input[1].length;
130             for (var i=0;i<numHits;i++) {
131               
132               if (! prefixesOnRight) {
133                 data[i] = {'left': tmu.textArrayToTextInfo(input[0][i]),
134                               'hit':  new tmu.TextInfo(input[1][i]),
135                               'right':tmu.textArrayToTextInfo(input[2][i]),
136                               'index':input[3][i],
137                               'id': i
138                               };
139               } else {
140                 //need to swap left and right and reverse
141                 var leftTI = tmu.textArrayToTextInfo(input[2][i]);
142                 leftTI.reverse();
143                 var rtTI = tmu.textArrayToTextInfo(input[0][i]);
144                 rtTI.reverse();
145                 data[i] = {'left': leftTI,
146                               'hit':  new tmu.TextInfo(input[1][i]),
147                               'right':rtTI,
148                               'index':input[3][i],
149                               'id': i
150                               };
151               }
152               
153             }
154             //data = realArray;
155         } else {
156             numHits = input.length;
157             //data = input;
158             data = input.map(function(d,i) {
159               d.id = i;
160               if (prefixesOnRight) {
161                 //swap left and right and reverse
162                 d.left.reverse();
163                 d.right.reverse();
164                 var tmp = d.left;
165                 d.right = d.left;
166                 d.left = tmp;
167               }
168               return d;
169             });
170         }
171         
172         filters = {};
173         excludedIDs = d3.set();
174         calcLastPage();
175         return kwic;
176     }
177     
178     /**
179      * @returns just the <em>ids</em> of the data that satisfies the current filters
180      */
181     this.filteredIDs = function() {
182         //return data.filter(function(d) {
183         //    return ! excludedIDs.has(d.id);
184         //}).map(function(d) {return d.id});
185         
186         var what = [];
187         for(var i=0,n=data.length;i<n;i++){
188             var id = d.id;
189             if (! excludedIDs.has(d.id)) {
190                 what.push(id);
191             }
192         }
193         return what;
194     }
195     
196     /**
197      * Getter/setter for the filters
198      * @param value is an object: {left: array of filter_functions, hit:filter_function, right: array of filter_functions.
199      * A filter_function gets an info object and returns Boolean. The function at index <em>i</em> filters position <em>i + 1</em> away from the root. Default is no filtering (via empty arrays).
200      * <p>
201      * Note: this does <strong>not</strong> redraw the KWIC. Use {@link kwicIS.KWIC#show} for that.
202      */
203     this.filters = function(value) {
204         if (!arguments.length) {
205             return filters;
206         }
207         filters = value;
208         filter();
209         return kwic;
210     }
211     
212     /**
213      * Getter/setter for the field names when the input is delimited strings
214      * @param value is an array of strings — the field names
215      */
216     this.fieldNames = function(value) {
217         if (!arguments.length) {
218             return fldNames;
219         }
220         fldNames = value;
221         tmu = textmodel.TextModelUtilities(fldNames, fldDelim, textInfoToString);
222         return kwic;
223     }
224     
225     /**
226      * Getter/setter for the delimiter when the input is delimited strings
227      * @param value is a string
228      */
229     this.fieldDelim = function(value) {
230         if (!arguments.length) {
231             return fldDelim;
232         }
233         fldDelim = value;
234         tmu = textmodel.TextModelUtilities(fldNames, fldDelim, textInfoToString);
235         return kwic;
236     }
237     
238     /**
239      * Getter/setter for the toString function of TextInfor when the input is delimited strings
240      * @param value is a string
241      */
242     this.textInfoToString = function(value) {
243         if (!arguments.length) {
244             return textInfoToString;
245         }
246         textInfoToString = value;
247         tmu = textmodel.TextModelUtilities(fldNames, fldDelim, textInfoToString);
248         return kwic;
249     }
250     
251     /**
252      * Getter/setter for the number of results to show per page
253      * @param value is an integer
254      * <p>
255      * Note: this does <strong>not</strong> redraw the KWIC. Use {@link kwicIS.KWIC#show} for that.
256      */
257     this.numToShow = function(value) {
258         if (!arguments.length) {
259             return numToShow;
260         }
261         numToShow = value;
262         calcLastPage();
263         return kwic;
264     }
265     
266     /**
267      * Getter/setter for the number of the current page of results
268      * @param value is an integer
269      * <p>
270      * Note: this does <strong>not</strong> redraw the KWIC. Use {@link kwicIS.KWIC#show} for that.
271      */
272     this.whichPage = function(value) {
273         if (!arguments.length) {
274             return whichPage;
275         }
276         whichPage = value;
277         return kwic;
278     }
279     
280     /**
281      * Getter/setter for the length of the left-hand context
282      * @param value is an integer
283      * <p>
284      * Note: this does <strong>not</strong> redraw the KWIC. Use {@link kwicIS.KWIC#show} for that.
285      */
286     this.leftContextLen = function(value) {
287         if (!arguments.length) {
288             return leftContextLen;
289         }
290         leftContextLen = value;
291         setupHeaderData();
292         return kwic;
293     }
294     
295     /**
296      * Getter/setter for the length of the right-hand context
297      * @param value is an integer
298      * <p>
299      * Note: this does <strong>not</strong> redraw the KWIC. Use {@link kwicIS.KWIC#show} for that.
300      */
301     this.rtContextLen = function(value) {
302         if (!arguments.length) {
303             return rtContextLen;
304         }
305         rtContextLen = value;
306         setupHeaderData();
307         return kwic;
308     }
309     
310     /**
311      * Convenience Getter/setter when both contexts are the same length
312      * @param value is an integer
313      * <p>
314      * Note: this does <strong>not</strong> redraw the KWIC. Use {@link kwicIS.KWIC#show} for that.
315      */
316     this.contextLen = function(value) {
317         if (!arguments.length) {
318             return rtContextLen;
319         }
320         leftContextLen = value;
321         rtContextLen = value;
322         setupHeaderData();
323         return kwic;
324     }
325     
326     /**
327      * Getter/setter for the script is right to left
328      * @param value is a boolean: true means the script is written right to left
329      * <p>
330      * Note: this does <strong>not</strong> redraw the KWIC. Use {@link kwicIS.KWIC#show} for that.
331      */
332     this.rtToLeft = function(value) {
333         if (!arguments.length) {
334             return prefixesOnRight;
335         }
336         prefixesOnRight = value;
337         return kwic;
338     }
339     
340     
341     /**
342      * @returns the number of hits for these (possibly filtered) results
343      */
344     this.hitsCount = function() {
345         return numHits;
346     }
347     
348      /**
349      * Returns the number of hits for these (possibly filtered) results, aggregated by the given field
350      * @param field is the field to use for the aggregation
351      * @returns a hash of the results
352      */
353     this.hitsByField = function(field) {
354         //return hash of field's values and their counts
355         
356         var what = {};
357         
358         data.filter(function(d) {
359             return ! excludedIDs.has(d.id);
360         }).forEach(function(d) {
361             var which = d.hit[field];
362             if (typeof(what[which]) === 'undefined') {
363                 what[which] = 0;
364             }
365            what[ which ]++;
366         });
367         return what;
368         
369     }
370     
371     /**
372      * Returns the actual fields used in the item objects
373      * @returns a d3.set
374      */
375     this.actualFields = function() {
376         if (typeof(data) != 'undefined' && data.length > 0) {
377             return d3.set(Object.keys(data[0].hit));
378         }
379         return d3.set();
380     }
381     
382     /**
383      * Sets the function to be used for the mouse hover title
384      * @param theFun is a function(itemObject, i) and returns a string. <em>i</em> is the number <em>in the current page</em>
385      */
386     this.cellTitle = function(theFun) {
387         cellTitle = theFun;
388         return kwic;
389     }
390     
391     /**
392      * Sets the function to be called when the column header is clicked
393      * @param theFun is a function(headerContent, column)
394      */
395     this.colHeaderClick = function(theFun) {
396         colHeaderClick = theFun;
397         return kwic;
398     }
399     
400     /**
401      * Sets the function to be called when an item cell is clicked
402      * @param theFun is a function(itemObject, i).  <em>i</em> is the number <em>in the current page</em> 
403      *
404      */
405     this.itemCellClick = function(theFun) {
406         itemCellClick = theFun;
407         return kwic;
408     }
409     
410     //setter only
411     //theFun(textInfoArray, hitNumber)
412     /**
413      * Sets the function to display a string for the row in an extra column, and whether the column is on the right or left end of the row. Default is no extra string.
414      * @param theFun is a function(itemObjectArray, hitNumber) and returns a string
415      * @param onRight is <em>boolean</em> (not just truthy) for position of rowString — true is on the right and false is on the left. Default is on the right.
416      */
417     this.rowString = function(theFun, onRight) {
418         rowString = theFun;
419         if (typeof(onRight) == "boolean") {
420             rowStringOnRight = onRight;
421         } else {
422             rowStringOnRight = true; //default
423         }
424         haveRowString = true;
425         setupHeaderData();
426         return kwic;
427     }
428     
429     
430     //other functions
431     
432     
433     /**
434      * Sorts the data according to the given column and the relevant settings for field, direction, and order
435      * @param column the column to be sorted by
436      * @param settings optional object with keys:
437      * <ul>
438      * <li>field: the field to use as the sort key</li>
439      * <li>sortFun: function(a,b) where a and be are the values of <em>field</em> and returns -1,0,1 according to whether a is less than, equal to, or greater than b.
440      * Default is a case insensitive lexicographic sort
441      * </li>
442      * <li>reverse: true if the order of sortFun should be reversed. Default is false.</li>
443      * </ul>
444      */
445     this.sort = function(column, settings) {
446         settings = settings || {};
447         var field;
448         var reverse = false;
449         var sortFun;
450         
451         if (settings.hasOwnProperty('field')) {
452             field = settings['field'];
453         }
454         if (settings.hasOwnProperty('reverse')) {
455             reverse = settings['reverse'];
456         }
457         if (settings.hasOwnProperty('sortFun')) {
458             sortFun = settings['sortFun'];
459         }
460         
461         if (typeof(sortFun) === 'undefined') {
462             sortFun = function(a,b) {
463                 var lowerA = a.toLocaleLowerCase();
464                 var lowerB = b.toLocaleLowerCase();
465                 //return lowerA < lowerB ? -1 : lowerA > lowerB ? 1 : 0;
466                 return lowerA.localeCompare(lowerB);
467             }
468         }
469         
470         
471         var hitCol = leftContextLen +1; //user columns are 1 based
472         if (haveRowString && ! rowStringOnRight) {
473             column--;
474         }
475         data.sort(function(obj1, obj2) {
476            
477            //get the relevant element
478             var elt1, elt2;
479             if (column == hitCol) {
480                 elt1 = obj1['hit'];
481                 elt2 = obj2['hit'];
482             } else if (column < hitCol) {
483                 var which = column-1;
484                 elt1 = obj1['left'][which];
485                 elt2 = obj2['left'][which];
486             } else  {
487                 var which = column-(leftContextLen +2); //user columns are 1 based
488                 elt1 = obj1['right'][which];
489                 elt2 = obj2['right'][which];
490             }
491             
492             //get the relevant string from the element
493             var comp1, comp2;
494            
495             if (typeof(elt1) === 'undefined') {
496                 comp1 = "";
497             } else {
498                 if (typeof(field) !== 'undefined') {
499                     if (typeof(elt1[field]) === 'undefined') {
500                         comp1 = "";
501                     } else {
502                         comp1 = elt1[field];
503                     }
504                 } else {
505                     comp1 = elt1.toString();
506                 }
507             }
508             if (typeof(elt2) === 'undefined') {
509                 comp2 = "";
510             } else {
511                 if (typeof(field) !== 'undefined') {
512                     if (typeof(elt2[field]) === 'undefined') {
513                         comp2 = "";
514                     } else {
515                         comp2 = elt2[field];
516                     }
517                 } else {
518                     comp2 = elt2.toString();
519                 }
520             }
521             
522             return sortFun(comp1, comp2);
523             
524         });
525         
526         if (reverse) {
527             data.reverse();
528         }
529         
530         kwic.showPage(1);
531     }
532     
533     //pageNum is 1-based for users, but internally we use 0-based
534     /**
535      * Show the given page of results
536      * @param pageNum is an integer. Page numbers are 1-based.
537      */
538     this.showPage = function(pageNum) {
539         kwic.whichPage(pageNum -1).show();
540     }
541     
542     /**
543      * Show the next page of results
544      */
545     this.showNextPage = function() {
546         if (whichPage == lastPage) {
547             return;
548         }
549         whichPage += 1;
550         kwic.show();
551     }
552     
553     /**
554      * Show the previous page of results
555      */
556     this.showPrevPage = function() {
557         if (whichPage == 0) {
558             return;
559         }
560         kwic.whichPage( kwic.whichPage()-1 ).show();
561     }
562     
563     /**
564      * Show the first page of results
565      */
566     this.showFirstPage = function() {
567         kwic.whichPage(0).show();
568     }
569     
570     /**
571      * Show the last page of results
572      */
573     this.showLastPage = function() {
574         kwic.whichPage(lastPage).show();
575     }
576     
577     /**
578      * Show the current page of results. This is the function that does the work of showing the KWIC.
579      */
580     this.show = function() {
581         
582         //select items for this page, trimming left and rt context, return flattened array
583         var startElt = numToShow*whichPage;
584         var thisData = data.filter(function(d) {
585             return ! excludedIDs.has(d.id);
586         })
587         .slice(startElt, startElt + numToShow).map(function(d, i){
588             d.left = d.left.slice(-leftContextLen);
589             //pad if it is too short
590             while (d.left.length < leftContextLen) {
591                 d.left.unshift("");
592             }
593             d.right = d.right.slice(0, rtContextLen);
594             //pad if too short
595             while (d.right.length < rtContextLen) {
596                 d.right.push("");
597             }
598            
599             var what = d.left.concat([d.hit], d.right);
600             var rowStr;
601             if (! prefixesOnRight) {
602                 rowStr = [ rowString(what, startElt + i) ];
603             } else {
604                 var tmpWhat = what.slice();
605                 tmpWhat.reverse();
606                 rowStr = [ rowString(tmpWhat, startElt + i) ];
607             }
608             
609             
610             
611             //var rowStr = [ rowString(what, startElt + i) ];
612             if (haveRowString) {
613                 if (rowStringOnRight) {
614                     what = what.concat(rowStr);
615                 } else {
616                     what = rowStr.concat(what);
617                 }
618             }
619             return {'cells':what, 'index': d.index, 'id':d.id};
620         
621         });
622         
623         headers = d3.select(where).select(" table > thead > tr").selectAll("th")
624             .data(headerData);
625             
626         headers.enter()
627             .append("th")
628             .text(String)
629             .classed("kwicTH", true)
630             .classed("kwicTHHit", function(d,i){
631                 return d == "HIT"; //from setupHeaderData    
632             })
633             .classed("kwicRowSummary", function(d,i) {
634                 return isSummaryCol(i);
635             })
636             .on("click", colHeaderClick);
637         
638         
639         /* https://github.com/d3/d3/blob/master/CHANGES.md#selections-d3-selection
640         var circle = svg.selectAll("circle").data(data) // UPDATE
641 			.style("fill", "blue");
642 
643 		circle.exit().remove(); // EXIT
644 
645 		circle.enter().append("circle") // ENTER
646 			.style("fill", "green")
647 		  .merge(circle) // ENTER + UPDATE
648 			.style("stroke", "black");
649     
650 	    */
651 	   
652 	    
653 	    //tr
654 	    //UPDATE
655         var tr = d3.select(where).select(" table > tbody").selectAll("tr")
656             .data(thisData);
657             
658         //EXIT
659         tr.exit().remove();
660         
661         var tre = tr.enter().append("tr") //ENTER
662          .merge(tr) //ENTER + UPDATE
663             .attr("index", function(d) {
664             	return d.index;
665         });
666         
667 
668         //td
669        	//UPDATE
670         var td = tre.selectAll("td")
671             .data(function(d) {
672                 return d.cells;
673             });
674             
675         td.enter().append("td")	//ENTER
676             .on("click", itemCellClick)
677          .merge(td) //ENTER + UPDATE
678             //td.html(function(d) {
679             .html(function(d) {
680                 return d.toString()})
681             .attr("class","kwicTD")
682             .classed("kwicTDHit", function(d,i) {
683                 var isHit;
684                 if (!haveRowString || rowStringOnRight) {
685                     isHit = (i == leftContextLen);
686                 } else if (haveRowString) {
687                     isHit = (i == (1+ leftContextLen));
688                 }
689                 return isHit;    
690             })
691             .classed("kwicRowSummary", function(d,i) {
692                 return isSummaryCol(i);
693             })
694             .attr("title", cellTitle);
695 
696 
697         //pass the hit's id and index from the row to the cell
698         d3.select(where).selectAll('.kwicTDHit')
699             .each(function(d) {
700                var p = d3.select(this.parentNode);
701                 
702                 d.id = +p.attr('id');
703                 d.index = p.attr('index');
704                 
705                 return d;
706             });
707             
708         
709         //EXIT
710         //td.exit().remove(); //we don't need this since tr.exit() removes the whole row
711         
712         return;
713     }
714     
715     //convenience filter function factories 
716 
717     /**
718      * Convenience filter function factory for including items whose field is val
719      * @param field is the field to use
720      * @param val is the value to test
721      * @returns a function(itemObject) which returns true if itemObject[field] === val and false otherwise
722      */
723     this.include_filter_function = function(field, val) {
724         return function(d) {
725             return d[field] === val;
726         }
727     }
728     
729     //return a function that includes an item where the field does NOT have the value
730      /**
731      * Convenience filter function factory for excluding items whose field is val
732      * @param field is the field to use
733      * @param val is the value to test
734      * @returns a function(itemObject) which returns true if itemObject[field] !== val and false otherwise
735      */
736     this.exclude_filter_function = function(field, val) {
737         return function(d) {
738             return d[field] !== val;
739         }
740     }
741     
742     ///////////     private functions
743     
744     function setupHeaderData() {
745         //set up the headers
746         headerData = [];
747         for(var i=0;i<leftContextLen;i++) {
748             if (! prefixesOnRight) {
749                 headerData.push("-" + (leftContextLen -i))
750             } else {
751                 headerData.push("+" + (leftContextLen -i))
752             }
753             
754         }
755         headerData.push("HIT");
756         for(var i=0;i<rtContextLen;i++) {
757             if (!prefixesOnRight) {
758                 headerData.push("+" + (1+i))
759             } else {
760                 headerData.push("-" + (1+i))
761             }
762             
763         }
764         if (haveRowString) {
765             if (rowStringOnRight) {
766                 headerData.push("Summary");
767             } else {
768                 headerData.unshift("Summary");
769             }    
770         }
771         
772     }
773     
774     function isSummaryCol(i) {
775         if (haveRowString) {
776             if (rowStringOnRight) {
777                 return i == headers.size()-1;
778             } else if (i == 0) {
779                 return true;
780             }
781         }
782         return false;
783     }
784     
785     function calcLastPage() {
786         numHits = data.length - excludedIDs.size();
787         if (!data || numHits === 0) {
788             lastPage = 0;
789         } else {
790             lastPage = Math.floor((numHits-1) / numToShow) ;
791         }
792     }
793     
794     function filter() {
795         excludedIDs = d3.set();
796         
797         
798         var hitHasFilter = typeof(filters.hit) === 'function';
799         var haveFilter = hitHasFilter;
800         
801         var leftHasFilter = [];
802         for (var i=0, n=data[0].left.length; i<n; i++) {
803             leftHasFilter[i] = typeof(filters.left) !== 'undefined' && typeof(filters.left[i]) === 'function';
804             haveFilter = haveFilter || leftHasFilter[i];
805         }
806         
807         var rightHasFilter = [];
808         for (var i=0, n=data[0].left.length; i<n; i++) {
809             rightHasFilter[i] = typeof(filters.right) !== 'undefined' && typeof(filters.right[i]) === 'function';
810             haveFilter = haveFilter || rightHasFilter[i];
811         }
812         
813         //shortcut if we have no filters
814         if (! haveFilter) {
815             calcLastPage();
816             return;
817         }
818         
819         for(var i=0, n=data.length;i<n;i++) {
820             var d = data[i];
821             
822             if (hitHasFilter && ! filters.hit(d.hit)) {
823                 excludedIDs.add(d.id);
824                 continue;
825             }
826             
827             var foundOne = false;
828             for(var j=0, m=d.left.length; j<m; j++) {
829                 if (leftHasFilter[j] && ! filters.left[j](d.left[m-j-1])) {
830                     excludedIDs.add(d.id);
831                     foundOne = true;
832                     break;
833                 }
834             }
835             if (foundOne) {
836                 continue;
837             }
838             
839             for(var j=0, m=d.right.length; j<m; j++) {
840                 if (rightHasFilter[j] && ! filters.right[j](d.right[j])) {
841                     excludedIDs.add(d.id);
842                     break;
843                 }
844             }
845             
846         }
847         calcLastPage();
848     }
849     
850 }
851 
852 })()