1 /* (This is the new BSD license.)
  2 * Copyright (c) 2012-2014, 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 "use strict";
 28 /**
 29  @namespace doubletree
 30  All of the functionality is in the doubletree namespace
 31 */
 32 var doubletree = {};
 33 
 34 (function(){
 35   
 36   //TBD what about visWidth? to be fixed visWidth is really width and width is really just the width of one side of the doubletree
 37   
 38   /**
 39     @class doubletree.DoubleTree
 40     This is the class for the DoubleTree visualization
 41   */
 42   doubletree.DoubleTree = function() {
 43     var containers = []; //nominally for allowing the same tree in multiple places, but not tested and probably doesn't work right (e.g. for search)
 44     //defaults. see below for getter/setters
 45     var visWidth = 600;
 46     var visHt; //calculated, not settable
 47     var prefixesOnRight = false; //true for right to left writing systems
 48     var filters = {"left":[], "right":[]};
 49     var handlers = {"alt":noOp, "shift":noOp};
 50     var showTokenExtra = true;
 51     var scaleLabels = true;
 52     var sortFun = doubletree.sortByStrFld("token");
 53     var nodeText = doubletree.tokenText; //default
 54     var tokenExtraText = function(info) {
 55       return doubletree.fieldText(info, "POS");
 56     };
 57     var rectColor = function(info) {
 58       return "rgba(255,255,255,0)"; //transparent white
 59     };
 60     var rectBorderColor = function(info) {
 61       return "rgba(255,255,255,0)"; //transparent white
 62     };
 63     var continuationColor = function(info) {
 64 	 return "red";
 65     }
 66     var basicStyles = {"node":{"fill":"white", "stroke":"steelblue", "stroke-width":"1.5px"}, "branch":{"stroke":"#aaa", "stroke-width":"1.5px"}};
 67     
 68     
 69     
 70     var succeeded = false; //did we succeed in building a DoubleTree? we need this flag, since we no longer return true/false from setupFromX (since we do chaining)
 71     
 72     var dispatch = d3.dispatch("idsUpdated");
 73     dispatch.on("idsUpdated", function() {
 74 	if (this == leftTree) {
 75 	  rtTree.setIds( leftTree.continuationIDs );
 76 	  rtTree.updateContinuations();
 77 	} else if (this == rtTree) {
 78 	  leftTree.setIds( rtTree.continuationIDs );
 79 	  leftTree.updateContinuations();
 80 	}	 
 81     });
 82   
 83     var leftTrie, rtTrie, leftTree, rtTree;
 84     var visibleIDs; //the ids of the results that are showing
 85     
 86     //tmp, until we can do sizing right. the font sizes are specified in doubletree.css and manually copied here
 87     var kFontSize = 14; //normal
 88     var kBigFontSize = 1.15*kFontSize; //for found text and continuations (was 18) NB: this is 0.05 bigger than in doubletree.css
 89     var kMinFontSize = 8; //smallest that we'll scale to
 90     var textScale;
 91     
 92     /** @exports mine as doubletree.DoubleTree */
 93     /** @ignore */
 94     function mine(selection) {
 95       // generate container and data independent part of chart here, using `width` and `height` etc
 96       
 97       selection.each(function(d, i) {
 98 	//generate chart here; `d` is the data and `this` is the element
 99 	//really, storing containers. Use updateData and redraw to really do the generation
100 	containers.push(this[i]);
101       });
102     
103     }
104     
105     /**
106      * initialize the visualization in one or more html containers
107      * <p>
108      * @param containerPattern CSS selector for the containers
109      */
110     mine.init = function(containerPattern) {
111       d3.select(d3.selectAll(containerPattern)).call(this);
112       return mine;
113     }
114     
115     /**
116      * redraw the visualization
117      */
118     mine.redraw = function() {
119       mine.setupFromTries(leftTrie, rtTrie);
120       
121       return mine;
122     }
123     
124 
125     /**
126      * set up the visualization using 2 {@link doubletree.Trie}s
127      * @param leftOne the left {@link doubletree.Trie}
128      * @param rtOne the right {@link doubletree.Trie}
129      */
130     mine.setupFromTries = function(leftOne, rtOne) {
131       leftTrie = leftOne.getUniqRoot();
132       rtTrie = rtOne.getUniqRoot();
133       
134       
135       
136       var leftTrieTree =  leftTrie.toTree(filters.left);
137       var rtTrieTree =  rtTrie.toTree(filters.right);
138      
139       var copyIDs = true;
140       if (Object.keys(rtTrieTree.pruned).length > 0) {
141         new_pruneTree(rtTrieTree, rtTrieTree.pruned, copyIDs);
142         new_pruneTree(leftTrieTree, rtTrieTree.pruned, copyIDs);
143         copyIDs = false;
144       }
145       
146       if (Object.keys(leftTrieTree.pruned).length > 0 ) {
147         new_pruneTree(leftTrieTree, leftTrieTree.pruned, copyIDs);
148         new_pruneTree(rtTrieTree, leftTrieTree.pruned, copyIDs);
149       }
150       
151       
152       //combine the info's from the two trees
153       var newInfo = {}; //rtTrieTree.info;
154       
155       for(var k in rtTrieTree.info) {
156         if (k != "continuations" && k != "ids" && k != "count") {
157           newInfo[k] = rtTrieTree.info[k];
158         }
159       }
160      
161       newInfo["right continuations"] = rtTrieTree.info.continuations;
162       newInfo["left continuations"] = leftTrieTree.info.continuations;
163       
164      
165       newInfo.ids = {};
166       addTo(newInfo.ids, rtTrieTree.info.ids);
167       addTo(newInfo.ids, leftTrieTree.info.ids);
168       newInfo.count = Object.keys(newInfo.ids).length;
169       visibleIDs = Object.keys(newInfo.ids);
170      
171       if (rtTrieTree.info.origIDs || leftTrieTree.info.origIDs) {
172         newInfo.origIDs = {};
173         addTo(newInfo.origIDs, rtTrieTree.info.origIDs);
174         addTo(newInfo.origIDs, leftTrieTree.info.origIDs);
175         newInfo.origCount = Object.keys(newInfo.origIDs).length;
176       }
177      
178      
179       rtTrieTree.info = newInfo;
180       leftTrieTree.info = newInfo;
181       
182       
183       
184       var maxChildren = Math.max(leftTrieTree.maxChildren, rtTrieTree.maxChildren);
185       if (isNaN(maxChildren) || maxChildren == 0 ) {
186         succeeded = false;
187         return mine;
188       }
189       
190       if (scaleLabels) {
191         textScale = d3.scale.log().range([kMinFontSize, kFontSize]);
192       } else {
193         textScale = function() {return kFontSize;}
194         textScale.domain = function(){};
195       }
196       
197       visHt = Math.max(200, maxChildren * (kBigFontSize-2));//TBD ?? fix was 16; 18 is the continuation font size
198       
199       var maxLen = Math.max(leftTrieTree.maxLen, rtTrieTree.maxLen);
200       var brLen = Math.max(80, maxLen*0.6*kBigFontSize); //TBD ?? fix was 10.5; 18 is the continuation font size
201       if (brLen > 200) {
202 	brLen = 200;
203       }
204       
205       var minCount = Math.min(leftTrieTree.minCount, rtTrieTree.minCount);
206       textScale.domain([minCount, leftTrieTree.info.count]);
207       
208       //TBD ?? margin, width, height, duplicated in Tree
209       var margin = {top: 20, right: 20, bottom: 20, left: 20},
210       width = visWidth - margin.right - margin.left,
211       height = visHt - margin.top - margin.bottom;
212       
213       containers[0].forEach(function(d,i) {
214 	var thisContainer = d;
215 	var thisVis;
216      
217      function zoomF() {
218       if (d3.event.sourceEvent.type !== 'mousemove') {
219           return;
220         }
221         d3.select(thisContainer).select("svg > g").attr("transform", "translate(" + d3.event.translate + ")"); //scale(" + d3.event.scale + ") //don't zoom
222      }
223        
224 	var tmp = d3.select(thisContainer).select("svg");
225 	if (tmp[0][0] == null) {
226 	  thisVis = d3.select(thisContainer).append("svg")
227 	    .attr("width", width + margin.right + margin.left)
228 	    .attr("height", height + margin.top + margin.bottom)
229          .attr("cursor", "move")
230           //should use drag, but didn't work right -- keeps bouncing back
231           /*
232           .call(d3.behavior.drag()
233             .origin(function() {return {'x':width/2, 'y':height/2}})
234             .on("drag", function() {
235                  var delta = [d3.event.dx, d3.event.dy];
236                  thisVis.attr("transform", "translate(" + delta + ")");
237               }));
238           */
239           .call(d3.behavior.zoom()
240             .on("zoom", function() {
241               if (d3.event.sourceEvent.type !== 'mousemove') {
242                 return;
243               }
244               d3.select(thisContainer).select("svg > g").attr("transform", "translate(" + d3.event.translate + ")"); //scale(" + d3.event.scale + ") //don't zoom
245             }));
246          
247 	  thisVis.append("g"); //container for both trees
248        
249          
250 	} else {
251 	  thisVis = tmp;
252 	  thisVis.attr("width", width + margin.right + margin.left)
253 	    .attr("height", height + margin.top + margin.bottom);
254 	  thisVis.selectAll("g *").remove(); //clear previous 
255 	}
256 	
257 	leftTree = new doubletree.Tree(thisVis.select("g"), visWidth, visHt, brLen, leftTrieTree, true, sortFun, dispatch, textScale, showTokenExtra, nodeText, tokenExtraText, rectColor, rectBorderColor, continuationColor, basicStyles);
258 	rtTree = new doubletree.Tree(thisVis.select("g"), visWidth, visHt, brLen, rtTrieTree, false, sortFun, dispatch, textScale, showTokenExtra, nodeText, tokenExtraText, rectColor, rectBorderColor, continuationColor, basicStyles);
259       });
260       
261       leftTree.handleAltPress = handlers.alt;
262       rtTree.handleAltPress = handlers.alt;
263     
264       leftTree.handleShiftPress = handlers.shift;
265       rtTree.handleShiftPress = handlers.shift;
266 	
267       succeeded = true;
268       return mine;
269     }
270     
271     //hitArray is an array of items, prefixArray and suffixArray are arrays of arrays of items
272     /**
273      * set up the visualization from arrays corresponding to the hit, the prefix, and the suffix of a key word in context result.
274      * <p>
275      * The ith elements should correspond with each other.
276      * Each item consists of fields separated by a field delimiter.
277      * For example we might have word/tag (with / as the delimiter) or word\tlemma\tauthor (with tab (\t) as the delimiter)
278      * Only certain fields are relevant for deciding whether two items are to be considered the same (e.g. we might ignore an author field)
279      * @param prefixArray the array of arrays of the prefixes of the hits
280      * @param hitArray the array of the hits
281      * @param suffixArray the array of arrays of the suffixes of the hits
282      * @param idArray the array of ids of the hits (or null, if there are no ids for the hits)
283      * @param caseSensitive are the hits case sensitive
284      * @param fieldNames the names of the fields
285      * @param fieldDelim the field delimiter
286      * @param distinguishingFieldsArray the fields that determine identity
287      * @param prefixesOnRight display the prefixes on the right, for right-to-left writing systems. Default is false
288      */
289     mine.setupFromArrays = function(prefixArray, hitArray, suffixArray, idArray, caseSensitive, fieldNames, fieldDelim, distinguishingFieldsArray) {
290       
291       if (undefined == caseSensitive && leftTrie) {
292 	caseSensitive = leftTrie.caseSensitive();
293       }
294       if (undefined == fieldNames  && leftTrie) {
295 	fieldNames = leftTrie.fieldNames();
296       }
297       if (undefined == fieldDelim  && leftTrie) {
298 	fieldDelim = leftTrie.fieldDelim();
299       }
300       if (undefined == distinguishingFieldsArray  && leftTrie) {
301 	distinguishingFieldsArray = leftTrie.distinguishingFieldsArray();
302       }
303       
304       leftTrie = new doubletree.Trie(caseSensitive, fieldNames, fieldDelim, distinguishingFieldsArray);
305       rtTrie = new doubletree.Trie(caseSensitive, fieldNames, fieldDelim, distinguishingFieldsArray);
306       
307       var n = hitArray.length;
308       for(var i=0;i<n;i++) {
309         var thisID = idArray ? idArray[i] : i;
310         var thisHit = hitArray[i];
311         var thesePrefixes = prefixArray[i].slice();
312         var theseSuffixes = suffixArray[i].slice();
313         
314         thesePrefixes.push(thisHit);
315         thesePrefixes.reverse();
316         theseSuffixes.unshift(thisHit);
317         
318         /*
319         if (prefixesOnRight) { //e.g. for Arabic, Hebrew, N'Ko, ...
320           thesePrefixes.push(thisHit);
321           thesePrefixes.reverse();
322           
323           rtTrie.addNgram( thesePrefixes, thisID);
324           
325           theseSuffixes.unshift(thisHit);
326           leftTrie.addNgram( theseSuffixes, thisID);
327           
328         } else {
329           thesePrefixes.push(thisHit);
330           thesePrefixes.reverse();
331           leftTrie.addNgram( thesePrefixes, thisID);
332           
333           theseSuffixes.unshift(thisHit);
334           rtTrie.addNgram( theseSuffixes, thisID);
335         }
336         */
337         
338         if (prefixesOnRight) {
339           rtTrie.addNgram( thesePrefixes, thisID);
340           leftTrie.addNgram( theseSuffixes, thisID);
341         } else {
342           leftTrie.addNgram( thesePrefixes, thisID);
343           rtTrie.addNgram( theseSuffixes, thisID);
344         }
345       }
346       
347       
348       mine.setupFromTries(leftTrie, rtTrie);
349       return mine;
350     }
351     
352     /**
353      * @returns just the <em>ids</em> of the data that satisfies the current filters
354      */
355     mine.filteredIDs = function() {
356       return visibleIDs;
357     }
358     
359     //return how many found
360     /**
361      * search the nodes of the visualization for a pattern
362      * <p>
363      * The found nodes will get the CSS class foundText
364      * @param searchRE the regular expression to look for
365      * @returns how many nodes were found
366      */
367     mine.search = function(searchRE) {
368       leftTree.search(searchRE);
369       rtTree.search(searchRE);
370       
371       var thisVis = d3.select(containers[0][0]);
372       var found = thisVis.selectAll("text.foundText");
373       
374       if (found.empty()) { return 0;}
375       
376       var what = found[0].length;
377      
378       var foundRt = thisVis.selectAll("text.rtNdText.foundText");
379       
380       if (foundRt[0][0] != null) {
381 	what--; //root node, and we have 2 of those, so subtract one from the count
382       }
383       return what;
384     }
385 
386     /**
387      * clear the visualization of the search results
388      * <p>
389      * the CSS class foundText is removed
390      */
391     mine.clearSearch = function() {
392 	leftTree.clearSearch();
393 	rtTree.clearSearch();
394 	return mine;
395     }
396     
397     /**
398      * update the showing/hiding of extra information associated with the basic item, e.g. part of speech information
399      * <p>
400      * Notes:
401      * 	<ul>
402      * 		<li>This <em>DOES</em> redraw the visualization.</li>
403      * 		<li>Safari does not update the visualization correctly by itself, so we force it to rebuild the entire visualization, unlike in other browsers.</li>
404      * 	</ul>
405      */
406     mine.updateTokenExtras = function() {
407       leftTree.showTokenExtras(showTokenExtra);
408       rtTree.showTokenExtras(showTokenExtra);
409       
410       //Safari doesn't update reshowing correctly, so we'll force it to build this again :(    (Chrome works correctly, so it's not a webkit issue)
411       var thisVis = d3.select(containers[0][0]);
412       var tokExtra = thisVis.select('.tokenExtra[display="inline"]');
413       if (! tokExtra.empty()) {
414 	var ht = tokExtra.style("height");
415 	if (ht == "0px") {
416 	  mine.redraw();
417 	}
418       }
419 
420       return mine;
421     }
422   
423     //////////// getter/setters
424     /**
425      * Getter/setter for the maximum width of the DoubleTree area
426      * @param value the maximum width
427      */
428     mine.visWidth = function(value) {
429       if (!arguments.length) return visWidth;
430       visWidth = value;
431       return mine;
432     }
433     
434     /**
435      * Getter/setter for whether the prefixes are displayed on the right or the left.
436      * <p>
437      * The default value is false, i.e. the prefixes are displayed on the left, as in English.
438      * <em>prefixesOnRight</em> should be set to true for right-to-left writing systems
439      * such as Arabic, Hebrew, N'Ko, etc.
440      * @param value true or false
441      */
442     mine.prefixesOnRight = function(value) {
443       if (!arguments.length) return prefixesOnRight;
444       prefixesOnRight = value;
445       return mine;
446     }
447     
448     //NB: doesn't redraw
449     /**
450      * Getter/setter for the filter functions.
451      *<p>
452      * The filter functions get an information object as their argument, and return true/false.
453      * Each position away from the root has its own filter, and the left and right sides also have their own filters.
454      * The filters are specified via an object with "left" and "right" keys whose values are arrays of functions
455      * The function at index <em>i</em> filters position <em>i + 1</em> away from the root.
456      * Default is no filtering (via empty arrays)
457      * <p>
458      * Note: setting the filters does <em>not</em> redraw the visualization. See {@link #redraw}
459      * @param value an object containing the filters
460      */ 
461     mine.filters = function(value) {
462       if (!arguments.length) return filters;
463       filters = value;
464       return mine;
465     }
466     
467     /**
468      * Getter/setter for the handlers for alt-click and shift-click on the nodes.
469      * <p>
470      * The handlers get an information object as their argument.
471      * The handlers are specified via an object with "alt" and "shift" keys whose values are functions
472      * The default is no handlers, i.e. <em>NO</em> interaction
473      * @param value an object containing the handlers
474      */
475     mine.handlers = function(value) {
476       if (!arguments.length) return handlers;
477       handlers = value;
478       return mine;
479     }
480     
481     //NB: doesn't redraw
482     /**
483      * Getter/setter for showing/hiding extra information associated with the main value, e.g. part of speech information.
484      * <p>
485      * Note: setting this value does <em>not</em> redraw the visualization. See {@link #redraw}
486      * Default is true
487      * @param value a boolean specifying whether to show the information or not
488      */
489     mine.showTokenExtra = function(value) {
490       if (!arguments.length) return showTokenExtra;
491       showTokenExtra = value;
492       return mine;
493     }
494     
495     /**
496      * Getter/setter for scaling the node labels by their frequency.
497      * <p>
498      * Default is true
499      * @param value a boolean specifying whether to scale the labels or not
500      */
501     mine.scaleLabels = function(value) {
502       if (!arguments.length) return scaleLabels;
503       scaleLabels = value;
504       return mine;
505     }
506     
507     //succeeded is read only
508     /**
509      * Reports whether the DoubleTree was constructed successfully
510      * <p>
511      * @returns true if the DoubleTree was constructed successfully and false otherwise
512      */
513     mine.succeeded = function() {
514       return succeeded;
515     }
516     
517     /**
518      * Getter/setter for the function determining the sort order of sibling nodes.
519      * <p>
520      * The function gets an information object as its argument, and should return -1 for precedes, 1 for follows and 0 for don't care
521      * The nodes are displayed in "preceding" (i.e. ascending) order, from top to bottom.
522      * The default is alphabetical by a "token" field if there is one: doubletree.sortByStrFld("token")
523      * @param the sort order function
524      */
525     mine.sortFun = function(value) {
526       if (!arguments.length) return sortFun;
527       sortFun = value;
528       return mine;
529     }
530     
531     /**
532      * Getter/setter for the function determining the content of the node labels.
533      * <p>
534      * The function gets an information object as its first argument and a boolean indicating whether the node is the root or not as its second argument. The function should return a string.
535      * The default is {@link #tokenText}
536      * @param the content function
537      */
538     mine.nodeText = function(value) {
539       if (!arguments.length) return nodeText;
540       nodeText = value;
541       return mine;
542     }
543     
544     /**
545      * Getter/setter for the function determining the content of the "extra" information for the labels labels
546      * <p>
547      * The function gets an information object as its first argument and a boolean indicating whether the node is the root or not as its second argument. The function should return a string.
548      * The default is the POS field of the information object
549      * @param the content function
550      */
551     mine.tokenExtraText = function(value) {
552       if (!arguments.length) return tokenExtraText;
553       tokenExtraText = value;
554       return mine;
555     }
556     
557     /**
558      * Getter/setter for the function determining the color of the background rectangles for the nodes.
559      * <p>
560      * The function gets an information object as its argument, and should return a CSS color in a string, e.g. "rgba(255,128,0,0.5)"
561      * The default is transparent white (i.e., effectively no color);
562      * @param value the background color function
563      */
564     mine.rectColor = function(value) {
565       if (!arguments.length) return rectColor;
566       rectColor = value;
567       return mine;
568     }
569     
570      /**
571      * Getter/setter for the function determining the color of the borders of the background rectangles for the nodes.
572      * <p>
573      * The function gets an information object as its argument, and should return a CSS color in a string, e.g. "rgba(255,128,0,0.5)"
574      * The default is transparent white (i.e., effectively no color);
575      * @param value the border color function
576      */
577     mine.rectBorderColor = function(value) {
578       if (!arguments.length) return rectBorderColor;
579       rectBorderColor = value;
580       return mine;
581     }
582     
583     /**
584      * Getter/setter for the function determining the color of the text of the nodes that are continuations of the clicked node.
585      * <p>
586      * The function gets an information object as its argument, and should return a CSS color in a string, e.g. "rgba(255,128,0,0.5)"
587      * The default is transparent white (i.e., effectively no color);
588      * @param value the border color function
589      */
590     mine.continuationColor = function(value) {
591       if (!arguments.length) return continuationColor;
592       continuationColor = value;
593       return mine;
594     }
595     
596     /**
597      * Getter/setter for the styles of the nodes and branches. For now these are constant throughout the tree.
598      * Takes an object of the form: {"node":{"fill":cssColor, "stroke":cssColor, "stroke-width":cssWidth}, "branch":{"stroke":cssColor, "stroke-width":cssWidth}}
599      * All of the attributes are optional
600      * Defaults are: {"node":{"fill":"white", "stroke":"steelblue", "stroke-width":"1.5px"}, "branch":{"stroke":"#777", "stroke-width":"1.5px"}};
601      */
602     mine.basicStyles = function(stylesObj) {
603       if (!arguments.length) return basicStyles;
604       
605       Object.keys(basicStyles).forEach(function(aspect){
606         if (aspect in stylesObj) {
607           Object.keys(basicStyles[aspect]).forEach(function(attr){
608             if (attr in stylesObj[aspect]) {
609               basicStyles[aspect][attr] = stylesObj[aspect][attr];
610             }
611           });
612         }
613       });
614       return mine;
615     }
616     
617     return mine;
618   }
619   
620   //////// tree for doubletree
621   /** @private */
622   doubletree.Tree = function(vis, visWidth, visHt, branchW, data, toLeft, sortFun, dispatch, textScale, showTokenXtra, nodeTextFun, tokenExtraTextFun, rectColorFun, rectBorderFun, contColorFun, baseStyles) {
623     var branchWidth = branchW;
624     var showTokenExtra = false || showTokenXtra;
625     var continuationIDs = {};
626     var clickedNode;
627     var nodeText = nodeTextFun;
628     var tokenExtraText = tokenExtraTextFun;
629     var rectColor = rectColorFun;
630     var rectBorderColor = rectBorderFun;
631     var continuationColor = contColorFun;
632     var basicStyles = baseStyles;
633     
634     var margin = {top: 20, right: 20, bottom: 20, left: 20},
635       width = visWidth - margin.right - margin.left,
636       height = visHt - margin.top - margin.bottom,
637       i = 0,
638       duration = 200,
639       root;
640     var dx;
641   
642     if (! sortFun ) {
643       sortFun = doubletree.sortByStrFld("token");
644     }
645     
646     var tree = d3.layout.tree()
647 	.size([height, width])
648 	.sort( sortFun )
649 	;
650     
651     var diagonal = d3.svg.diagonal()
652 	//.projection(function(d) { return [d.y, d.x]; }); //CC orig
653 	.projection(function(d) { return [positionX(d.y), positionY(d.x)]; });
654     
655     vis = vis.append("g")
656 	.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
657     
658     
659     ////////
660     this.readJSONTree = function(json) {
661       root = json;
662       root.x0 = height / 2;  
663       root.y0 = width / 2; //CC orig was 0
664     
665       root.children.forEach(collapse);
666       this.update(root);
667     }
668     
669     //CC had been inside readJSONTree
670     function collapse(d) {
671       if (d.children) {
672 	d._children = d.children;
673 	d._children.forEach(collapse);
674 	d.children = null;
675       }
676     }
677     
678     //CC new
679     function collapseSiblings(nd) {
680       if (nd.parent) {
681 	nd.parent.children.forEach(function(d) {
682 	    if (d != nd) {
683 	      collapse(d);
684 	    }
685 	});
686       }  
687     }
688     
689     var that = this;
690 
691     this.update = function(source) {      
692       if (! source ) {
693 	source = root;
694       }
695   
696       // Compute the new tree layout.
697       var nodes = tree.nodes(root).reverse(); //CC orig why reverse?
698       //var nodes = tree.nodes(root);
699       
700       //we don't want the root to change position, so we need to compensate
701       dx = root.x - height/2;     
702     
703     
704       // Normalize for fixed-depth.
705       nodes.forEach(function(d) { d.y = d.depth * branchWidth; }); //TBD paramaterize -- this is the length of the branch ??
706     
707       // Update the nodes…
708       var node = vis.selectAll("g.node_" + toLeft)
709 	  .data(nodes, function(d) { return d.id || (d.id = ++i); });
710     
711       // Enter any new nodes at the parent's previous position.
712       var nodeEnter = node.enter().append("g")
713 	  .attr("class", "node node_" + toLeft)	
714 	  .attr("transform", function(d) { return "translate(" + positionX(source.y0) + "," + positionY(source.x0) + ")"; })
715 	  /* doesn't work for webkit; svg really wants the title as separate element, see below
716 	   .attr("title", function(d) {
717 	    var what = doubletree.infoToText(d.info);
718 	    return what;})
719 	  */
720 	  .on("click", click);
721     
722       nodeEnter.append("title")
723 	.text(function(d) {
724 	    var what = doubletree.infoToText(d.info);
725 	    return what;}
726       );
727     
728       nodeEnter.append("circle")
729 	  .attr("r", 1e-6)	  
730 	  .style("fill", function(d) { return d._children ? "#fff" : basicStyles.node.fill; })
731 	  .style("stroke", function(d) { return basicStyles.node.stroke});
732     
733       var txtNode = nodeEnter.append("text")
734 	  .attr("class", function(d){
735 	      if (d.depth == 0) {
736 		return "rtNdText";
737 	      } else {
738 		return "";
739 	      }
740 	    })	
741 	  .attr("x", function(d) {
742 	    if (d.children || d._children) {	      	      
743 	      return 0;
744 	    } else {
745 	      return toLeft ? 10 : -10;
746 	    }	    
747 	  })
748 	  .attr("text-anchor", function(d) {
749 	    if (! d.parent) {
750 	      return "middle";
751 	    }
752 	    if (d.children || d._children) {
753 	      return toLeft ? "end" : "start";	      
754 	    } else {
755 	      return toLeft ? "start" : "end";
756 	    }	    
757 	  })
758 	  .style("font-size", function(d) {
759             /*
760             if (d.depth == 0 && toLeft) {
761               return 0; //suppress left side root -- do this because of differences in counts when filtering
762             }
763             */
764 	      return textScale(d.info.count) + "pt";
765 	    });
766 	  
767       txtNode.append("tspan")
768 	  .attr("dy", ".35em")
769 	  .attr("class", "tokenText")
770 	  .text(function(d) {
771 		return nodeText(d.info, d.depth < 1); })
772 	  .style("fill-opacity", 1e-6);
773       
774 	txtNode.append("tspan")
775 	  .attr("dx", ".35em")
776 	  .attr("class", "tokenExtra")
777 	  .text(function(d) {return tokenExtraText(d.info, d.depth < 1); })
778 	  .style("fill-opacity", 1e-6);
779       
780       this.drawRects = function() {
781 	var which = showTokenExtra ? "inline" : "none";
782 	vis.selectAll(".tokenExtra").attr("display",which);
783 	
784 	node.selectAll("rect").remove(); //remove previous rects
785 
786 	var nodeRect = node.append("rect")
787 	    .attr("class", "nodeRect")
788 	   .attr("height", function(){
789 	      return this.parentElement.getBBox().height -6;
790 	   })
791 	   .attr("y", function(d){
792 	      if (! d.parent) {
793 		return -0.5* this.parentElement.getBBox().height/2 -2;
794 	      } else {
795 		return -0.5* this.parentElement.getBBox().height/2 ;
796 	      }
797 	    })
798 	   .attr("width", function(){
799 	      return this.parentElement.getBBox().width;
800 	   })
801 	   .attr("x", function(d){
802 	      var parentW = this.parentElement.getBBox().width;
803 	      if (! d.parent) {
804 		return -0.33333 * parentW;
805 	      }
806 	      if (!toLeft) {
807 		return 0;
808 	      }
809 	      return -0.5 * parentW;
810 	   })	 
811 	   //.style("stroke-opacity", 1e-6)
812 	   .style("stroke-opacity", 1)
813 	   .style("stroke-width", 1)
814 	   .style("stroke", function(d){
815 	      return rectBorderColor(d.info);
816 	    })
817 	   .style("fill", function(d) {
818 	      return rectColor(d.info);
819 	   })
820 	   .style("fill-opacity", function(d){
821 	      if (! d.parent && ! toLeft) {
822 		return 1e-6;
823 	      } else {
824 		return 1;
825 	      }
826 	    });
827       }
828       try {
829 	this.drawRects();
830       } catch (e) {
831 	//apparently we're in some version of Opera, which thinks "this" is the window, not the rect wh
832       }
833     
834       // Transition nodes to their new position.
835       var nodeUpdate = node.transition()	  
836 	  .duration(duration)
837 	  .attr("transform", function(d) { return "translate(" + positionX(d.y) + "," + positionY(d.x) + ")"; });
838     
839       nodeUpdate.select("circle")
840 	  //.attr("r", 4.5)
841 	  .attr("r", function(d) { return (d.children || d._children) ? 1e-6 : 4.5})
842 	  .style("fill", function(d) { return d._children ? "#fff" : basicStyles.node.fill; }) //function(d) { return d._children ? "lightsteelblue" : "#fff"; })
843 	  .style("stroke-width", basicStyles.node["stroke-width"]);
844     
845       nodeUpdate.select("text")
846 	    .attr("class", function(d) {
847 	    	var isContinuation = containedIn(that.continuationIDs, d.info.ids);
848 	    	//var classes = this.classList.toString(); //TBD ?? WARNING, classList is Firefox only
849 
850 			if (isContinuation || !d.parent) {
851 				//this.classList.add("continuation");
852 				classListAdd(this,"continuation");
853 			} else {
854 				//this.classList.remove("continuation");
855 				classListRemove(this,"continuation");
856 			}
857 			//return this.classList.toString();
858 			return classListToString(this);
859 	    })
860 	  .style("fill-opacity", 1)
861 	  .style("fill", function(d){
862 		if (classListContains(this,"continuation")) {
863 		  return continuationColor(d.info);
864 		}
865 		return "#444"; //default text color
866 	   }); //this is duplicate in updateContinuations
867 
868       nodeUpdate.selectAll("tspan")
869 	.style("fill-opacity", 1);
870 
871     
872       // Transition exiting nodes to the parent's new position.
873       var nodeExit = node.exit().transition()
874 	  .duration(duration)
875 	  .attr("transform", function(d) { return "translate(" + positionX(source.y) + "," + positionY(source.x) + ")"; })
876 	  .remove();
877     
878       nodeExit.select("circle")
879 	  .attr("r", 1e-6);
880     
881       //nodeExit.select("text")
882       nodeExit.selectAll("tspan")
883 	  .style("fill-opacity", 1e-6);
884     
885       // Update the links…
886       var link = vis.selectAll("path.link_" + toLeft)
887 	  .data(tree.links(nodes), function(d) { return d.target.id; });
888     
889       // Enter any new links at the parent's previous position.
890       link.enter().insert("path", "g")
891 	  .attr("class", "link link_" + toLeft)
892 	  .attr("d", function(d) {
893 	    var o = {x: source.x0, y: source.y0}; //CC orig	
894 	    return diagonal({source: o, target: o});
895 	  })
896 	  .style("fill", "none")
897 	  .style("stroke", basicStyles.branch.stroke)
898 	  .style("stroke-width",basicStyles.branch["stroke-width"]);
899     
900       // Transition links to their new position.
901       link.transition()
902 	  .duration(duration)
903 	  .attr("d", diagonal);
904     
905       // Transition exiting nodes to the parent's new position.
906       link.exit().transition()
907 	  .duration(duration)
908 	  .attr("d", function(d) {
909 	    var o = {x: source.x, y: source.y}; //CC orig	    
910 	    return diagonal({source: o, target: o}); //CC orig	  
911 	  })
912 	  .remove();
913     
914       // Stash the old positions for transition.
915       nodes.forEach(function(d) {
916 	d.x0 = d.x;
917 	d.y0 = d.y;
918       });
919       
920     }
921   
922     // Toggle children on click.
923     function click(d, i) {
924       if (d3.event.altKey) {
925 	that.handleAltPress(d,i);
926 	//that.showTokenExtras(showTokenExtra);
927 	return;
928       }
929       if (d3.event.shiftKey) {
930 	that.handleShiftPress(d,i);
931 	//that.showTokenExtras(showTokenExtra);
932 	return;
933       }
934       
935       if (! d.parent ) {
936 	return;
937       }
938       if (that.continuationIDs != d.info.ids) {
939 	that.setIds(d.info.ids);
940 	that.clickedNode = d.id;
941 	dispatch.idsUpdated.apply(that);
942       }
943       
944       collapseSiblings(d); //CC new    
945       /*
946       if (d.children) {
947 	d._children = d.children;
948 	d.children = null;
949       } else {
950 	d.children = d._children;
951 	d._children = null;
952       }
953       that.update(d);
954       */
955       toggleChildren(d, true);
956     }
957     
958     function toggleChildren(d, update) { //we only update the clicked node, not recursively
959       //collapseSiblings(d); //CC we don't do this here, since after the top level there's no point
960       
961       if (d.children) {
962         if (d.children && d.children.length == 1) {
963           toggleChildren(d.children[0], true); //need true to make sure we toggle all the way down
964         }
965         d._children = d.children;
966         d.children = null;
967         
968         
969       } else {
970         d.children = d._children;
971         d._children = null;
972        
973         //expand all if there is only one path
974         if (d.children && d.children.length == 1) {
975           toggleChildren(d.children[0], false);
976         }
977       }
978       if (update) {
979          that.update(d);
980       }
981     }
982     
983     this.setIds = function(ids) {
984       that.continuationIDs = ids;
985     }    
986     this.updateContinuations = function() {
987       vis.selectAll("g.node_" + toLeft + " text")
988 	.classed("continuation", function(d) {
989         var isContinuation = overlap(d.info.ids, that.continuationIDs);
990         if (isContinuation) {
991           classListAdd(this,"continuation");  //some day we can get rid of this ...
992         } else {
993           classListRemove(this, "continuation");
994         }
995 	   return isContinuation;
996 	})
997 	.style("fill", function(d){
998 		if (classListContains(this,"continuation")) {
999 		  return continuationColor(d.info);
1000 		}
1001 		return "#444"; //default text color
1002 	 }); //this is duplicated from above, nodeUpdate
1003     }
1004     
1005     this.search = function(searchRE) {
1006       vis.selectAll("g.node text")
1007 	.attr("class", function(d) {
1008 	    var what = searchRE.test(nodeText(d.info));
1009 	    if (what) {
1010 		    //this.classList.add("continuation");
1011 		    classListAdd(this,"foundText");
1012 	    } else {
1013 		    //this.classList.remove("continuation");
1014 		    classListRemove(this,"foundText");
1015 	    }
1016 	    //return this.classList.toString();
1017 	    return classListToString(this);	    
1018 	})
1019     }
1020     
1021     this.clearSearch = function() {
1022       vis.selectAll("g.node text")
1023 	.attr("class", function(d) {
1024 	    classListRemove(this,"foundText");
1025 	    return classListToString(this);	    
1026 	});
1027     }
1028     
1029     this.showTokenExtras = function(show) {
1030       if (arguments.length == 0) {
1031 	return showTokenExtra;
1032       }
1033       showTokenExtra = show;
1034       
1035       this.drawRects();
1036       return this;
1037     }
1038     
1039     this.setRectColor = function(rectColorFun) {
1040       if (arguments.length == 0) {
1041 	return rectColor;
1042       }
1043       rectColor = rectColorFun;
1044       this.drawRects();
1045       return this;
1046     }
1047     
1048     ///////////////
1049     function positionX(x) {
1050       return toLeft ? width/2-x : width/2+ x;
1051     }
1052     function positionY(y) {
1053       return y -dx;
1054     }
1055     
1056     
1057     
1058     ////default modifier handlers
1059     this.handleAltPress = function() {};
1060     this.handleShifttPress = function() {};
1061     
1062     this.readJSONTree(data);
1063     return this;
1064   }
1065   
1066 
1067 
1068   ///////////////////////////////// tree sorting functions
1069   /**
1070    * function to sort the nodes (case insenstive) by a field in the information object
1071    * @param fld the field to sort by
1072    */
1073   doubletree.sortByStrFld = function(fld) {
1074     var field = fld;
1075     return function(a,b) {
1076 	var aUndefined = (undefined == a.info[field]);
1077 	var bUndefined = (undefined == b.info[field]);
1078 	if (aUndefined && bUndefined) {
1079 	  return 0;
1080 	} else if (aUndefined) {
1081 	  return -1;
1082 	} else if (bUndefined) {
1083 	  return 1;
1084 	}
1085 	var aVal = a.info[field].join(" ").toLowerCase();
1086 	var bVal = b.info[field].join(" ").toLowerCase();
1087 	  if (aVal < bVal) {
1088 	    return -1;
1089 	  } else if (aVal > bVal) {
1090 	    return 1;
1091 	  }
1092 	  return 0;
1093       }
1094   }
1095   /**
1096    * function to sort the nodes according to the count field in the information object
1097    */
1098   doubletree.sortByCount = function() {
1099     return function(a,b) {
1100 	return b.info.count - a.info.count;
1101     }
1102   }
1103   
1104   /**
1105    * function to sort the nodes according to the continuations field in the information object
1106    */
1107   doubletree.sortByContinuations = function() {
1108     return function(a,b) {
1109 	return b.info.continuations - a.info.continuations;
1110     }
1111   }
1112   
1113   
1114   ///////////////////////////////// some tree filtering functions
1115   /**
1116    * function to filter the nodes according to a minimum for the count field
1117    * @param n the minimum count to include
1118    */
1119   doubletree.filterByMinCount = function(n) {
1120       return function(inf) { return inf.count >= n;};
1121   }
1122   
1123   /**
1124    * function to filter the nodes according to a maximum for the count field
1125    * @param n the maximum count to include
1126    */
1127   doubletree.filterByMaxCount = function(n) {
1128       return function(inf) { return inf.count <= n;};
1129   }
1130   
1131   /**
1132    * function to filter the nodes according to the "POS" field (if it exists)
1133    * @param n a string for a regular expression of the POS values to include
1134    */
1135   doubletree.filterByPOS = function(pos) {
1136     var re = new RegExp(pos);
1137     return function(inf) {
1138       return inf["POS"] && inf["POS"].filter(function(p) {
1139 	  return p.search(re) > -1;
1140 	}).length > 0; //end of ng has no POS
1141       }
1142   }
1143   
1144   ///////////////////////////////// formatting functions
1145 
1146   //doubletree.nodeText = function(info) {
1147   //  return doubletree.tokenText(info); //default
1148   //}
1149   
1150   //extracts a field
1151   /**
1152    * return the value of a field in the provided information object
1153    * @param info the information object
1154    * @param the field to get
1155    * @returns the value of the field in the information object
1156    */
1157   doubletree.fieldText = function(info, fieldName) {
1158     return info[fieldName];
1159   }
1160   //extracts the "token" field
1161   /**
1162    * convenience function to return the value of the "token" field (if it exists). The same as doubletree.fieldText(info, "token")
1163    * @param info the information object
1164    * @returns the value of the "token" field of the information object
1165    */
1166   doubletree.tokenText = function(info) {
1167     return doubletree.fieldText(info, "token");
1168   }
1169   
1170   /**
1171    * converts an information object to a string
1172    * @param the information object
1173    * @returns a string with one key/value pair per line
1174    */
1175   doubletree.infoToText = function(info) {
1176       var what = "";
1177       for(var infp in info) {
1178 	if (infp == "ids" || infp == "origIDs") {
1179 	  what += infp + "\t:\t" + Object.keys( info[infp] ).join(",") + "\n";
1180 	} else {
1181 	  what += (infp + "\t:\t" + info[infp] + "\n");
1182 	}
1183       }
1184       return what;
1185   }
1186   ////////////////// internal utility functions
1187 
1188   function old_pruneTree(tree, ids) {
1189     
1190     if (! tree.children) {
1191         return;
1192     }
1193     
1194     var n = tree.children.length;
1195     for(var i=0;i<n;i++) {
1196         var c = tree.children[i];
1197         
1198         if (containedIn(c.info.ids, ids)) {
1199             tree.children[i] = null;
1200         } else {
1201             old_pruneTree(c, ids);
1202         }
1203     }
1204     tree.children = tree.children.filter(function(c) { return c != null});
1205     
1206     //recalculate maxChildren
1207     var cMax = d3.max( tree.children.map(function(c) {return c.maxChildren;}) );
1208     tree.maxChildren = Math.max(tree.children.length, cMax);
1209   }
1210   
1211   
1212   function new_pruneTree(tree, ids, copyIDs) {
1213     
1214     if (! tree.children) {
1215         return;
1216     }
1217     
1218     //copy over original ids
1219     if (copyIDs) {
1220       if (! tree.info.origIDs) {
1221         tree.info.origIDs = {};
1222         addTo(tree.info.origIDs, tree.info.ids);
1223         tree.info.origCount = Object.keys(tree.info.origIDs).length;
1224       } else {
1225         tree.info.ids = {};
1226         addTo(tree.info.ids, tree.info.origIDs);
1227         tree.info.count = Object.keys(tree.info.ids).length;
1228       }
1229     }
1230    
1231     
1232     //adjust IDs
1233     var idNums = Object.keys(ids)
1234     for(var i=0, n=idNums.length;i<n;i++) {
1235       var cid = idNums[i];
1236       delete tree.info.ids[cid];
1237     }
1238     tree.info.count = Object.keys(tree.info.ids).length;
1239     
1240     //recurse and prune
1241     var n = tree.children.length;
1242     for(var i=0;i<n;i++) {
1243         var c = tree.children[i];
1244         
1245         if (containedIn(c.info.ids, ids)) {
1246             tree.children[i] = null;
1247         } else {
1248             new_pruneTree(c, ids, false);
1249         }
1250     }
1251     tree.children = tree.children.filter(function(c) { return c != null});
1252     tree.info.continuations = tree.children.length;
1253     
1254     //recalculate maxChildren
1255     var cMax = d3.max( tree.children.map(function(c) {return c.maxChildren;}) );
1256     tree.maxChildren = Math.max(tree.children.length, cMax);
1257   }
1258   
1259   function restoreTree(tree) {
1260     
1261       if (tree.info.origCount) {  //otherwise tree was suppressed, so its ids never got switched around
1262         //restore originals
1263         tree.info.ids = {};
1264         addTo(tree.info.ids, tree.info.origIDs);
1265         //delete tree.info.origIDs;
1266         tree.info.count = tree.info.origCount;
1267         //delete tree.info.origCount;
1268         
1269         var n = tree.children.length;
1270         tree.info.continuations = n;
1271         for(var i=0;i<n;i++) {
1272             var c = tree.children[i];
1273             restoreTree(c);
1274         }
1275       }
1276   
1277   }
1278   
1279   //do the keys in o1 and o2 overlap
1280   function overlap(o1, o2) {
1281       for(var k in o1) {
1282 	  if (k in o2) {
1283 	      return true;
1284 	  }
1285       }
1286       return false;
1287   }
1288   //are all the keys in o1 also in o2
1289   function containedIn(o1, o2) {
1290       if (! o1 || ! o2) {
1291 	  return false;
1292       }
1293       for(var k in o1) {
1294 	  if (! (k in o2)) {
1295 	      return false;
1296 	  }
1297       }
1298       return true; 
1299   }
1300   
1301   //add key/vals of o2 to o1 and return o1; (top level key-value only, o2 values maintained over o1)
1302   //same as in Trie.js
1303   function addTo(o1, o2) {
1304       for(var k in o2) {
1305           o1[k] = o2[k];
1306       }
1307   }
1308 
1309   function noOp() {}
1310 
1311   
1312   //////////////////
1313 
1314 
1315 })();