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 })();