PageRenderTime 167ms CodeModel.GetById 67ms app.highlight 87ms RepoModel.GetById 0ms app.codeStats 1ms

/Tutorial/example_html/src/krona-2.0.js

https://github.com/BioinformaticsArchive/metAMOS
JavaScript | 6290 lines | 4980 code | 851 blank | 459 comment | 843 complexity | 11f3a823b037886b4ed607b63ffa4f00 MD5 | raw file
Possible License(s): GPL-2.0

Large files files are truncated, but you can click here to view the full file

   1{//-----------------------------------------------------------------------------
   2// 
   3// PURPOSE
   4// 
   5// Krona is a flexible tool for exploring the relative proportions of
   6// hierarchical data, such as metagenomic classifications, using a
   7// radial, space-filling display. It is implemented using HTML5 and
   8// JavaScript, allowing charts to be explored locally or served over the
   9// Internet, requiring only a current version of any major web
  10// browser. Krona charts can be created using an Excel template or from
  11// common bioinformatic formats using the provided conversion scripts.
  12// 
  13// 
  14// COPYRIGHT LICENSE
  15// 
  16// Copyright (c) 2011, Battelle National Biodefense Institute (BNBI);
  17// all rights reserved. Authored by: Brian Ondov, Nicholas Bergman, and
  18// Adam Phillippy
  19// 
  20// This Software was prepared for the Department of Homeland Security
  21// (DHS) by the Battelle National Biodefense Institute, LLC (BNBI) as
  22// part of contract HSHQDC-07-C-00020 to manage and operate the National
  23// Biodefense Analysis and Countermeasures Center (NBACC), a Federally
  24// Funded Research and Development Center.
  25// 
  26// Redistribution and use in source and binary forms, with or without
  27// modification, are permitted provided that the following conditions are
  28// met:
  29// 
  30// * Redistributions of source code must retain the above copyright
  31//   notice, this list of conditions and the following disclaimer.
  32// 
  33// * Redistributions in binary form must reproduce the above copyright
  34//   notice, this list of conditions and the following disclaimer in the
  35//   documentation and/or other materials provided with the distribution.
  36// 
  37// * Neither the name of the Battelle National Biodefense Institute nor
  38//   the names of its contributors may be used to endorse or promote
  39//   products derived from this software without specific prior written
  40//   permission.
  41// 
  42// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  43// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  44// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  45// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  46// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  47// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  48// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  49// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  50// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  51// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  52// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  53// 
  54// 
  55// TRADEMARK LICENSE
  56// 
  57// KRONA(TM) is a trademark of the Department of Homeland Security, and use
  58// of the trademark is subject to the following conditions:
  59// 
  60// * Distribution of the unchanged, official code/software using the
  61//   KRONA(TM) mark is hereby permitted by the Department of Homeland
  62//   Security, provided that the software is distributed without charge
  63//   and modification.
  64// 
  65// * Distribution of altered source code/software using the KRONA(TM) mark
  66//   is not permitted unless written permission has been granted by the
  67//   Department of Homeland Security.
  68// 
  69// 
  70// FOR MORE INFORMATION VISIT
  71// 
  72// http://krona.sourceforge.net
  73// 
  74//-----------------------------------------------------------------------------
  75}
  76
  77var canvas;
  78var context;
  79var svg; // for snapshot mode
  80var collapse = true;
  81var collapseCheckBox;
  82var collapseLast;
  83var compress;
  84var compressCheckBox;
  85var maxAbsoluteDepthText;
  86var maxAbsoluteDepthButtonDecrease;
  87var maxAbsoluteDepthButtonIncrease;
  88var fontSize = 11;
  89var fontSizeText;
  90var fontSizeButtonDecrease;
  91var fontSizeButtonIncrease;
  92var fontSizeLast;
  93var shorten;
  94var shortenCheckBox;
  95var maxAbsoluteDepth;
  96var backButton;
  97var upButton;
  98var forwardButton;
  99var snapshotButton;
 100var snapshotMode = false;
 101var details;
 102var detailsName;
 103var search;
 104var searchResults;
 105var nSearchResults;
 106var useHueCheckBox;
 107var useHueDiv;
 108var datasetDropDown;
 109var datasetButtonLast;
 110var datasetButtonPrev;
 111var datasetButtonNext;
 112var keyControl;
 113var showKeys = true;
 114var linkButton;
 115var linkText;
 116var frame;
 117
 118// Node references. Note that the meanings of 'selected' and 'focused' are
 119// swapped in the docs.
 120//
 121var head; // the root of the entire tree
 122var selectedNode = 0; // the root of the current view
 123var focusNode = 0; // a node chosen for more info (single-click)
 124var highlightedNode = 0; // mouse hover node
 125var highlightingHidden = false;
 126var nodes = new Array();
 127var currentNodeID = 0; // to iterate while loading
 128
 129var nodeHistory = new Array();
 130var nodeHistoryPosition = 0;
 131
 132var dataEnabled = false; // true when supplemental files are present
 133
 134// store non-Krona GET variables so they can be passed on to links
 135//
 136var getVariables = new Array();
 137
 138// selectedNodeLast is separate from the history, since we need to check
 139// properties of the last node viewed when browsing through the history
 140//
 141var selectedNodeLast = 0;
 142var zoomOut = false;
 143
 144// temporary zoom-in while holding the mouse button on a wedge
 145//
 146var quickLook = false; // true when in quick look state
 147var mouseDown = false;
 148var mouseDownTime; // to detect mouse button hold
 149var quickLookHoldLength = 200;
 150
 151var imageWidth;
 152var imageHeight;
 153var centerX;
 154var centerY;
 155var gRadius;
 156var updateViewNeeded = false;
 157
 158// Determines the angle that the pie chart starts at.  90 degrees makes the
 159// center label consistent with the children.
 160//
 161var rotationOffset = Math.PI / 2;
 162
 163var buffer = 100;
 164
 165// The maps are the small pie charts showing the current slice being viewed.
 166//
 167var mapBuffer = 10;
 168var mapRadius = 0;
 169var maxMapRadius = 25;
 170var mapWidth = 150;
 171var maxLabelOverhang = Math.PI * 4.18;
 172
 173// Keys are the labeled boxes for slices in the highest level that are too thin
 174// to label.
 175//
 176var maxKeySizeFactor = 2; // will be multiplied by font size
 177var keySize;
 178var keys;
 179var keyBuffer = 10;
 180var currentKey;
 181var keyMinTextLeft;
 182var keyMinAngle;
 183
 184var minRingWidthFactor = 5; // will be multiplied by font size
 185var maxPossibleDepth; // the theoretical max that can be displayed
 186var maxDisplayDepth; // the actual depth that will be displayed
 187var headerHeight = 0;//document.getElementById('options').clientHeight;
 188var historySpacingFactor = 1.6; // will be multiplied by font size
 189var historyAlphaDelta = .25;
 190
 191// appearance
 192//
 193var lineOpacity = 0.3;
 194var saturation = 0.5;
 195var lightnessBase = 0.6;
 196var lightnessMax = .8;
 197var thinLineWidth = .3;
 198var highlightLineWidth = 1.5;
 199var labelBoxBuffer = 6;
 200var labelBoxRounding = 15;
 201var labelWidthFudge = 1.05; // The width of unshortened labels are set slightly
 202							// longer than the name width so the animation
 203							// finishes faster.
 204var fontNormal;
 205var fontBold;
 206var fontFamily = 'sans-serif';
 207//var fontFaceBold = 'bold Arial';
 208var nodeRadius;
 209var angleFactor;
 210var tickLength;
 211var compressedRadii;
 212
 213// colors
 214//
 215var highlightFill = 'rgba(255, 255, 255, .3)';
 216var colorUnclassified = 'rgb(220,220,220)';
 217
 218// label staggering
 219//
 220var labelOffsets; // will store the current offset at each depth
 221//
 222// This will store pointers to the last node that had a label in each offset (or "track") of a
 223// each depth.  These will be used to shorten neighboring labels that would overlap.
 224// The [nLabelNodes] index will store the last node with a radial label.
 225// labelFirstNodes is the same, but to check for going all the way around and
 226// overlapping the first labels.
 227//
 228var labelLastNodes;
 229var labelFirstNodes;
 230//
 231var nLabelOffsets = 3; // the number of offsets to use
 232
 233var mouseX = -1;
 234var mouseY = -1;
 235
 236// tweening
 237//
 238var progress = 0; // for tweening; goes from 0 to 1.
 239var progressLast = 0;
 240var tweenFactor = 0; // progress converted by a curve for a smoother effect.
 241var tweenLength = 850; // in ms
 242var tweenCurvature = 13;
 243//
 244// tweenMax is used to scale the sigmoid function so its range is [0,1] for the
 245// domain [0,1]
 246//
 247var tweenMax = 1 / (1 + Math.exp(-tweenCurvature / 2));
 248//
 249var tweenStartTime;
 250
 251// for framerate debug
 252//
 253var tweenFrames = 0;
 254var fpsDisplay = document.getElementById('frameRate');
 255
 256// Arrays to translate xml attribute names into displayable attribute names
 257//
 258var attributes = new Array();
 259//
 260var magnitudeIndex; // the index of attribute arrays used for magnitude
 261var membersAssignedIndex;
 262var membersSummaryIndex;
 263
 264// For defining gradients
 265//
 266var hueDisplayName;
 267var hueStopPositions;
 268var hueStopHues;
 269var hueStopText;
 270
 271// multiple datasets
 272//
 273var currentDataset = 0;
 274var lastDataset = 0;
 275var datasets = 1;
 276var datasetNames;
 277var datasetSelectSize = 30;
 278var datasetAlpha = new Tween(0, 0);
 279var datasetWidths = new Array();
 280var datasetChanged;
 281var datasetSelectWidth = 50;
 282
 283window.onload = load;
 284
 285var image;
 286var hiddenPattern;
 287var loadingImage;
 288
 289function resize()
 290{
 291	imageWidth = window.innerWidth;
 292	imageHeight = window.innerHeight;
 293	
 294	if ( ! snapshotMode )
 295	{
 296		context.canvas.width = imageWidth;
 297		context.canvas.height = imageHeight;
 298	}
 299	
 300	if ( datasetDropDown )
 301	{
 302		var ratio = 
 303			(datasetDropDown.offsetTop + datasetDropDown.clientHeight) * 2 /
 304			imageHeight;
 305		
 306		if ( ratio > 1 )
 307		{
 308			ratio = 1;
 309		}
 310		
 311		ratio = Math.sqrt(ratio);
 312		
 313		datasetSelectWidth = 
 314			(datasetDropDown.offsetLeft + datasetDropDown.clientWidth) * ratio;
 315	}
 316	var leftMargin = datasets > 1 ? datasetSelectWidth + 30 : 0;
 317	var minDimension = imageWidth - mapWidth - leftMargin > imageHeight ?
 318		imageHeight :
 319		imageWidth - mapWidth - leftMargin;
 320	
 321	maxMapRadius = minDimension * .03;
 322	buffer = minDimension * .1;
 323	margin = minDimension * .015;
 324	centerX = (imageWidth - mapWidth - leftMargin) / 2 + leftMargin;
 325	centerY = imageHeight / 2;
 326	gRadius = minDimension / 2 - buffer;
 327	//context.font = '11px sans-serif';
 328}
 329
 330function handleResize()
 331{
 332	updateViewNeeded = true;
 333}
 334
 335function Attribute()
 336{
 337}
 338
 339function Tween(start, end)
 340{
 341	this.start = start;
 342	this.end = end;
 343	this.current = this.start;
 344	
 345	this.current = function()
 346	{
 347		if ( progress == 1 || this.start == this.end )
 348		{
 349			return this.end;
 350		}
 351		else
 352		{
 353			return this.start + tweenFactor * (this.end - this.start);
 354		}
 355	};
 356	
 357	this.setTarget = function(target)
 358	{
 359		this.start = this.current();
 360		this.end = target;
 361	}
 362}
 363
 364function Node()
 365{
 366	this.id = currentNodeID;
 367	currentNodeID++;
 368	nodes[this.id] = this;
 369	
 370	this.angleStart = new Tween(Math.PI, 0);
 371	this.angleEnd = new Tween(Math.PI, 0);
 372	this.radiusInner = new Tween(1, 1);
 373	this.labelRadius = new Tween(1, 1);
 374	this.labelWidth = new Tween(0, 0);
 375	this.scale = new Tween(1, 1); // TEMP
 376	this.radiusOuter = new Tween(1, 1);
 377	
 378	this.r = new Tween(255, 255);
 379	this.g = new Tween(255, 255);
 380	this.b = new Tween(255, 255);
 381	
 382	this.alphaLabel = new Tween(0, 1);
 383	this.alphaLine = new Tween(0, 1);
 384	this.alphaArc = new Tween(0, 0);
 385	this.alphaWedge = new Tween(0, 1);
 386	this.alphaOther = new Tween(0, 1);
 387	this.alphaPattern = new Tween(0, 0);
 388	this.children = Array();
 389	this.parent = 0;
 390	
 391	this.attributes = new Array(attributes.length);
 392	
 393	this.addChild = function(child)
 394	{
 395		this.children.push(child);
 396	};
 397	
 398	this.addLabelNode = function(depth, labelOffset)
 399	{
 400		if ( labelHeadNodes[depth][labelOffset] == 0 )
 401		{
 402			// this will become the head node for this list
 403			
 404			labelHeadNodes[depth][labelOffset] = this;
 405			this.labelPrev = this;
 406		}
 407		
 408		var head = labelHeadNodes[depth][labelOffset];
 409		
 410		this.labelNext = head;
 411		this.labelPrev = head.labelPrev;
 412		head.labelPrev.labelNext = this;
 413		head.labelPrev = this;
 414	}
 415	
 416	this.canDisplayDepth = function()
 417	{
 418		// whether this node is at a depth that can be displayed, according
 419		// to the max absolute depth
 420		
 421		return this.depth <= maxAbsoluteDepth;
 422	}
 423	
 424	this.canDisplayHistory = function()
 425	{
 426		var radiusInner;
 427		
 428		if ( compress )
 429		{
 430			radiusInner = compressedRadii[0];
 431		}
 432		else
 433		{
 434			radiusInner = nodeRadius;
 435		}
 436		
 437		return (
 438			-this.labelRadius.end * gRadius +
 439			historySpacingFactor * fontSize / 2 <
 440			radiusInner * gRadius
 441			);
 442	}
 443	
 444	this.canDisplayLabelCurrent = function()
 445	{
 446		return (
 447			(this.angleEnd.current() - this.angleStart.current()) *
 448			(this.radiusInner.current() * gRadius + gRadius) >=
 449			minWidth());
 450	}
 451	
 452	this.checkHighlight = function()
 453	{
 454		if ( this.children.length == 0 && this == focusNode )
 455		{
 456			//return false;
 457		}
 458		
 459		if ( this.hide )
 460		{
 461			return false;
 462		}
 463		
 464		if ( this.radiusInner.end == 1 )
 465		{
 466			// compressed to the outside; don't check
 467			
 468			return false;
 469		}
 470		
 471		var highlighted = false;
 472		
 473		var angleStartCurrent = this.angleStart.current() + rotationOffset;
 474		var angleEndCurrent = this.angleEnd.current() + rotationOffset;
 475		var radiusInner = this.radiusInner.current() * gRadius;
 476		
 477		for ( var i = 0; i < this.children.length; i++ )
 478		{
 479			highlighted = this.children[i].checkHighlight();
 480			
 481			if ( highlighted )
 482			{
 483				return true;
 484			}
 485		}
 486		
 487		if ( this != selectedNode && ! this.getCollapse() )
 488		{
 489			context.beginPath();
 490			context.arc(0, 0, radiusInner, angleStartCurrent, angleEndCurrent, false);
 491			context.arc(0, 0, gRadius, angleEndCurrent, angleStartCurrent, true);
 492			context.closePath();
 493			
 494			if ( context.isPointInPath(mouseX - centerX, mouseY - centerY) )
 495			{
 496				highlighted = true;
 497			}
 498			
 499			if
 500			(
 501				! highlighted &&
 502				(angleEndCurrent - angleStartCurrent) *
 503				(radiusInner + gRadius) <
 504				minWidth() &&
 505				this.getDepth() == selectedNode.getDepth() + 1
 506			)
 507			{
 508				if ( showKeys && this.checkHighlightKey() )
 509				{
 510					highlighted = true;
 511				}
 512			}
 513		}
 514		
 515		if ( highlighted )
 516		{
 517			if ( this != highlightedNode )
 518			{
 519			//	document.body.style.cursor='pointer';
 520			}
 521			
 522			highlightedNode = this;
 523		}
 524		
 525		return highlighted;
 526	}
 527	
 528	this.checkHighlightCenter = function()
 529	{
 530		if ( ! this.canDisplayHistory() )
 531		{
 532			return;
 533		}
 534		
 535		var cx = centerX;
 536		var cy = centerY - this.labelRadius.end * gRadius;
 537		//var dim = context.measureText(this.name);
 538		
 539		var width = this.nameWidth;
 540		
 541		if ( this.searchResultChildren() )
 542		{
 543			var results = searchResultString(this.searchResultChildren());
 544			var dim = context.measureText(results);
 545			width += dim.width;
 546		}
 547		
 548		if
 549		(
 550			mouseX > cx - width / 2 &&
 551			mouseX < cx + width / 2 &&
 552			mouseY > cy - historySpacingFactor * fontSize / 2 &&
 553			mouseY < cy + historySpacingFactor * fontSize / 2
 554		)
 555		{
 556			highlightedNode = this;
 557			return;
 558		}
 559		
 560		if ( this.getParent() )
 561		{
 562			this.getParent().checkHighlightCenter();
 563		}
 564	}
 565	
 566	this.checkHighlightKey = function()
 567	{
 568		var offset = keyOffset();
 569		
 570		var xMin = imageWidth - keySize - margin - this.keyNameWidth - keyBuffer;
 571		var xMax = imageWidth - margin;
 572		var yMin = offset;
 573		var yMax = offset + keySize;
 574		
 575		currentKey++;
 576		
 577		return (
 578			mouseX > xMin &&
 579			mouseX < xMax &&
 580			mouseY > yMin &&
 581			mouseY < yMax);
 582	}
 583	
 584	this.checkHighlightMap = function()
 585	{
 586		if ( this.parent )
 587		{
 588			this.parent.checkHighlightMap();
 589		}
 590		
 591		if ( this.getCollapse() || this == focusNode )
 592		{
 593			return;
 594		}
 595		
 596		var box = this.getMapPosition();
 597		
 598		if
 599		(
 600			mouseX > box.x - mapRadius &&
 601			mouseX < box.x + mapRadius &&
 602			mouseY > box.y - mapRadius &&
 603			mouseY < box.y + mapRadius
 604		)
 605		{
 606			highlightedNode = this;
 607		}
 608	}
 609	
 610/*	this.collapse = function()
 611	{
 612		for (var i = 0; i < this.children.length; i++ )
 613		{
 614			this.children[i] = this.children[i].collapse();
 615		}
 616		
 617		if
 618		(
 619			this.children.length == 1 &&
 620			this.children[0].magnitude == this.magnitude
 621		)
 622		{
 623			this.children[0].parent = this.parent;
 624			this.children[0].getDepth() = this.parent.getDepth() + 1;
 625			return this.children[0];
 626		}
 627		else
 628		{
 629			return this;
 630		}
 631	}
 632*/	
 633	this.draw = function(labelMode, selected, searchHighlighted)
 634	{
 635		var depth = this.getDepth() - selectedNode.getDepth() + 1;
 636//		var hidden = false;
 637		
 638		if ( selectedNode == this )
 639		{
 640			selected = true;
 641		}
 642		
 643		var angleStartCurrent = this.angleStart.current() + rotationOffset;
 644		var angleEndCurrent = this.angleEnd.current() + rotationOffset;
 645		var radiusInner = this.radiusInner.current() * gRadius;
 646		var canDisplayLabelCurrent = this.canDisplayLabelCurrent();
 647		var hiddenSearchResults = false;
 648		
 649/*		if ( ! this.hide )
 650		{
 651			for ( var i = 0; i < this.children.length; i++ )
 652			{
 653				if ( this.children[i].hide && this.children[i].searchResults )
 654				{
 655					hiddenSearchResults = true;
 656				}
 657			}
 658		}
 659*/		
 660		var drawChildren =
 661			( ! this.hide || ! this.hidePrev && progress < 1 ) &&
 662			( ! this.hideAlone || ! this.hideAlonePrev && progress < 1 );
 663		
 664//		if ( this.alphaWedge.current() > 0 || this.alphaLabel.current() > 0 )
 665		{
 666			var lastChildAngleEnd;
 667			
 668			if ( this.hasChildren() )//canDisplayChildren )
 669			{
 670				lastChildAngleEnd =
 671					this.children[this.children.length - 1].angleEnd.current()
 672					+ rotationOffset;
 673			}
 674			
 675			if ( labelMode )
 676			{
 677				var drawRadial =
 678				!(
 679					this.parent &&
 680					this.parent != selectedNode &&
 681					angleEndCurrent == this.parent.angleEnd.current() + rotationOffset
 682				);
 683				
 684				if ( angleStartCurrent != angleEndCurrent )
 685				{
 686					this.drawLines(angleStartCurrent, angleEndCurrent, radiusInner, drawRadial, selected);
 687				}
 688				
 689				var alphaOtherCurrent = this.alphaOther.current();
 690				var childRadiusInner;
 691				
 692				if ( this == selectedNode || alphaOtherCurrent )
 693				{
 694					childRadiusInner =
 695						this.children[this.children.length - 1].radiusInner.current() * gRadius;
 696				}
 697				
 698				if ( this == selectedNode )
 699				{
 700					this.drawReferenceRings(childRadiusInner);
 701				}
 702				
 703				if
 704				(
 705					selected &&
 706					! searchHighlighted &&
 707					this != selectedNode &&
 708					(
 709						this.isSearchResult ||
 710						this.hideAlone && this.searchResultChildren() ||
 711						false
 712//						this.hide &&
 713//						this.containsSearchResult
 714					)
 715				)
 716				{
 717					context.globalAlpha = this.alphaWedge.current();
 718					
 719					drawWedge
 720					(
 721						angleStartCurrent,
 722						angleEndCurrent,
 723						radiusInner,
 724						gRadius,
 725						highlightFill,
 726						0,
 727						true
 728					);
 729					
 730					if
 731					(
 732						this.keyed &&
 733						! showKeys &&
 734						this.searchResults &&
 735						! searchHighlighted &&
 736						this != highlightedNode &&
 737						this != focusNode
 738					)
 739					{
 740						var angle = (angleEndCurrent + angleStartCurrent) / 2;
 741						this.drawLabel(angle, true, false, true, true);
 742					}
 743					
 744					//this.drawHighlight(false);
 745					searchHighlighted = true;
 746				}
 747				
 748				if
 749				(
 750					this == selectedNode ||
 751//					true
 752					//(canDisplayLabelCurrent) &&
 753					this != highlightedNode &&
 754					this != focusNode
 755				)
 756				{
 757					if ( this.radial != this.radialPrev && this.alphaLabel.end == 1 )
 758					{
 759						context.globalAlpha = tweenFactor;
 760					}
 761					else
 762					{
 763						context.globalAlpha = this.alphaLabel.current();
 764					}
 765					
 766					this.drawLabel
 767					(
 768						(angleStartCurrent + angleEndCurrent) / 2,
 769						this.hideAlone && this.searchResultChildren() ||
 770						(this.isSearchResult || hiddenSearchResults) && selected,
 771						this == selectedNode && ! this.radial,
 772						selected,
 773						this.radial
 774					);
 775					
 776					if ( this.radial != this.radialPrev && this.alphaLabel.start == 1 && progress < 1 )
 777					{
 778						context.globalAlpha = 1 - tweenFactor;
 779						
 780						this.drawLabel
 781						(
 782							(angleStartCurrent + angleEndCurrent) / 2,
 783							(this.isSearchResult || hiddenSearchResults) && selected,
 784							this == selectedNodeLast && ! this.radialPrev,
 785							selected,
 786							this.radialPrev
 787						);
 788					}
 789				}
 790				
 791				if
 792				(
 793					alphaOtherCurrent &&
 794					lastChildAngleEnd != null
 795				)
 796				{
 797					if
 798					(
 799						(angleEndCurrent - lastChildAngleEnd) *
 800						(childRadiusInner + gRadius) >=
 801						minWidth()
 802					)
 803					{
 804						//context.font = fontNormal;
 805						context.globalAlpha = this.alphaOther.current();
 806						
 807						drawTextPolar
 808						(
 809							this.getUnclassifiedText(),
 810							this.getUnclassifiedPercentage(),
 811							(lastChildAngleEnd + angleEndCurrent) / 2,
 812							(childRadiusInner + gRadius) / 2,
 813							true,
 814							false,
 815							false,
 816							0,
 817							0
 818						);
 819					}
 820				}
 821				
 822				if ( this == selectedNode && this.keyUnclassified && showKeys )
 823				{
 824					this.drawKey
 825					(
 826						(lastChildAngleEnd + angleEndCurrent) / 2,
 827						false,
 828						false
 829					);
 830				}
 831			}
 832			else
 833			{
 834				var alphaWedgeCurrent = this.alphaWedge.current();
 835				
 836				if ( alphaWedgeCurrent || this.alphaOther.current() )
 837				{
 838					var currentR = this.r.current();
 839					var currentG = this.g.current();
 840					var currentB = this.b.current();
 841						
 842					var fill = rgbText(currentR, currentG, currentB);
 843					
 844					var radiusOuter;
 845					var lastChildAngle;
 846					var truncateWedge =
 847					(
 848						this.hasChildren() &&
 849						! this.keyed &&
 850						(compress || depth < maxDisplayDepth) &&
 851						drawChildren
 852					);
 853					
 854					if ( truncateWedge )
 855					{
 856						radiusOuter = this.children[0].radiusInner.current() * gRadius;
 857					}
 858					else
 859					{
 860						radiusOuter = gRadius;
 861					}
 862					/*
 863					if ( this.hasChildren() )
 864					{
 865						radiusOuter = this.children[0].getUncollapsed().radiusInner.current() * gRadius + 1;
 866					}
 867					else
 868					{ // TEMP
 869						radiusOuter = radiusInner + nodeRadius * gRadius;
 870						
 871						if ( radiusOuter > gRadius )
 872						{
 873							radiusOuter = gRadius;
 874						}
 875					}
 876					*/
 877					context.globalAlpha = alphaWedgeCurrent;
 878					
 879					if ( radiusInner != radiusOuter )
 880					{
 881						drawWedge
 882						(
 883							angleStartCurrent,
 884							angleEndCurrent,
 885							radiusInner,
 886							radiusOuter,//this.radiusOuter.current() * gRadius,
 887							//'rgba(0, 200, 0, .1)',
 888							fill,
 889							this.alphaPattern.current()
 890						);
 891						
 892						if ( truncateWedge )
 893						{
 894							// fill in the extra space if the sum of our childrens'
 895							// magnitudes is less than ours
 896							
 897							if ( lastChildAngleEnd < angleEndCurrent )//&& false) // TEMP
 898							{
 899								if ( radiusOuter > 1 )
 900								{
 901									// overlap slightly to hide the seam
 902									
 903	//								radiusOuter -= 1;
 904								}
 905								
 906								if ( alphaWedgeCurrent < 1 )
 907								{
 908									context.globalAlpha = this.alphaOther.current();
 909									drawWedge
 910									(
 911										lastChildAngleEnd,
 912										angleEndCurrent,
 913										radiusOuter,
 914										gRadius,
 915										colorUnclassified,
 916										0
 917									);
 918									context.globalAlpha = alphaWedgeCurrent;
 919								}
 920								
 921								drawWedge
 922								(
 923									lastChildAngleEnd,
 924									angleEndCurrent,
 925									radiusOuter,
 926									gRadius,//this.radiusOuter.current() * gRadius,
 927									//'rgba(200, 0, 0, .1)',
 928									fill,
 929									this.alphaPattern.current()
 930								);
 931							}
 932						}
 933						
 934						if ( radiusOuter < gRadius )
 935						{
 936							// patch up the seam
 937							//
 938							context.beginPath();
 939							context.arc(0, 0, radiusOuter, angleStartCurrent/*lastChildAngleEnd*/, angleEndCurrent, false);
 940							context.strokeStyle = fill;
 941							context.lineWidth = 1;
 942							context.stroke();
 943						}
 944					}
 945					
 946					if ( this.keyed && selected && showKeys )//&& progress == 1 )
 947					{
 948						this.drawKey
 949						(
 950							(angleStartCurrent + angleEndCurrent) / 2,
 951							(
 952								this == highlightedNode ||
 953								this == focusNode ||
 954								this.searchResults
 955							),
 956							this == highlightedNode || this == focusNode
 957						);
 958					}
 959				}
 960			}
 961		}
 962		
 963		if ( drawChildren )
 964		{
 965			// draw children
 966			//
 967			for ( var i = 0; i < this.children.length; i++ )
 968			{
 969				if ( this.drawHiddenChildren(i, selected, labelMode, searchHighlighted) )
 970				{
 971					i = this.children[i].hiddenEnd;
 972				}
 973				else
 974				{
 975					this.children[i].draw(labelMode, selected, searchHighlighted);
 976				}
 977			}
 978		}
 979	};
 980	
 981	this.drawHiddenChildren = function
 982	(
 983		firstHiddenChild,
 984		selected,
 985		labelMode,
 986		searchHighlighted
 987	)
 988	{
 989		var firstChild = this.children[firstHiddenChild];
 990		
 991		if ( firstChild.hiddenEnd == null || firstChild.radiusInner.current() == 1 )
 992		{
 993			return false;
 994		}
 995		
 996		for ( var i = firstHiddenChild; i < firstChild.hiddenEnd; i++ )
 997		{
 998			if ( ! this.children[i].hide || ! this.children[i].hidePrev && progress < 1 )
 999			{
1000				return false;
1001			}
1002		}
1003		
1004		var angleStart = firstChild.angleStart.current() + rotationOffset;
1005		var lastChild = this.children[firstChild.hiddenEnd];
1006		var angleEnd = lastChild.angleEnd.current() + rotationOffset;
1007		var radiusInner = gRadius * firstChild.radiusInner.current();
1008		var hiddenChildren = firstChild.hiddenEnd - firstHiddenChild + 1;
1009		
1010		if ( labelMode )
1011		{
1012			var hiddenSearchResults = 0;
1013			
1014			for ( var i = firstHiddenChild; i <= firstChild.hiddenEnd; i++ )
1015			{
1016				hiddenSearchResults += this.children[i].searchResults;
1017			}
1018			
1019			if
1020			(
1021				selected &&
1022				(angleEnd - angleStart) * 
1023				(gRadius + gRadius) >=
1024				minWidth() ||
1025				hiddenSearchResults
1026			)
1027			{
1028				context.globalAlpha = this.alphaWedge.current();
1029				
1030				this.drawHiddenLabel
1031				(
1032					angleStart,
1033					angleEnd,
1034					hiddenChildren,
1035					hiddenSearchResults
1036				);
1037			}
1038		}
1039		
1040		var drawWedges = true;
1041		
1042		for ( var i = firstHiddenChild; i <= firstChild.hiddenEnd; i++ )
1043		{
1044			// all hidden children must be completely hidden to draw together
1045			
1046			if ( this.children[i].alphaPattern.current() != this.children[i].alphaWedge.current() )
1047			{
1048				drawWedges = false;
1049				break;
1050			}
1051		}
1052		
1053		if ( labelMode )
1054		{
1055			if ( drawWedges )
1056			{
1057				var drawRadial = (angleEnd < this.angleEnd.current() + rotationOffset);
1058				this.drawLines(angleStart, angleEnd, radiusInner, drawRadial);
1059			}
1060			
1061			if ( hiddenSearchResults && ! searchHighlighted )
1062			{
1063				drawWedge
1064				(
1065					angleStart,
1066					angleEnd,
1067					radiusInner,
1068					gRadius,//this.radiusOuter.current() * gRadius,
1069					highlightFill,
1070					0,
1071					true
1072				);
1073			}
1074		}
1075		else if ( drawWedges )
1076		{
1077			context.globalAlpha = this.alphaWedge.current();
1078			
1079			var fill = rgbText
1080			(
1081				firstChild.r.current(),
1082				firstChild.g.current(),
1083				firstChild.b.current()
1084			);
1085			
1086			drawWedge
1087			(
1088				angleStart,
1089				angleEnd,
1090				radiusInner,
1091				gRadius,//this.radiusOuter.current() * gRadius,
1092				fill,
1093				context.globalAlpha,
1094				false
1095			);
1096		}
1097		
1098		return drawWedges;
1099	}
1100	
1101	this.drawHiddenLabel = function(angleStart, angleEnd, value, hiddenSearchResults)
1102	{
1103		var textAngle = (angleStart + angleEnd) / 2;
1104		var labelRadius = gRadius + fontSize;//(radiusInner + radius) / 2;
1105		
1106		drawTick(gRadius - fontSize * .75, fontSize * 1.5, textAngle);
1107		drawTextPolar
1108		(
1109			value.toString() + ' more',
1110			0, // inner text
1111			textAngle,
1112			labelRadius,
1113			true, // radial
1114			hiddenSearchResults, // bubble
1115			this == highlightedNode || this == focusNode, // bold
1116			false,
1117			hiddenSearchResults
1118		);
1119	}
1120	
1121	this.drawHighlight = function(bold)
1122	{
1123		var angleStartCurrent = this.angleStart.current() + rotationOffset;
1124		var angleEndCurrent = this.angleEnd.current() + rotationOffset;
1125		var radiusInner = this.radiusInner.current() * gRadius;
1126		
1127		//this.setHighlightStyle();
1128		
1129		if ( this == focusNode && this == highlightedNode && this.hasChildren() )
1130		{
1131//			context.fillStyle = "rgba(255, 255, 255, .3)";
1132			arrow
1133			(
1134				angleStartCurrent,
1135				angleEndCurrent,
1136				radiusInner
1137			);
1138		}
1139		else
1140		{
1141			drawWedge
1142			(
1143				angleStartCurrent,
1144				angleEndCurrent,
1145				radiusInner,
1146				gRadius,
1147				highlightFill,
1148				0,
1149				true
1150			);
1151		}
1152		
1153		// check if hidden children should be highlighted
1154		//
1155		for ( var i = 0; i < this.children.length; i++ )
1156		{
1157			if
1158			(
1159				this.children[i].getDepth() - selectedNode.getDepth() + 1 <=
1160				maxDisplayDepth &&
1161				this.children[i].hiddenEnd != null
1162			)
1163			{
1164				var firstChild = this.children[i];
1165				var lastChild = this.children[firstChild.hiddenEnd];
1166				var hiddenAngleStart = firstChild.angleStart.current() + rotationOffset;
1167				var hiddenAngleEnd = lastChild.angleEnd.current() + rotationOffset;
1168				var hiddenRadiusInner = gRadius * firstChild.radiusInner.current();
1169				
1170				drawWedge
1171				(
1172					hiddenAngleStart,
1173					hiddenAngleEnd,
1174					hiddenRadiusInner,
1175					gRadius,
1176					'rgba(255, 255, 255, .3)',
1177					0,
1178					true
1179				);
1180				
1181				if ( ! this.searchResults )
1182				{
1183					this.drawHiddenLabel
1184					(
1185						hiddenAngleStart,
1186						hiddenAngleEnd,
1187						firstChild.hiddenEnd - i + 1
1188					);
1189				}
1190				
1191				i = firstChild.hiddenEnd;
1192			}
1193		}
1194		
1195//			context.strokeStyle = 'black';
1196		context.fillStyle = 'black';
1197		
1198		var highlight = ! ( progress < 1 && zoomOut && this == selectedNodeLast );
1199		
1200		var angle = (angleEndCurrent + angleStartCurrent) / 2;
1201		
1202		if ( ! (this.keyed && showKeys) )
1203		{
1204			this.drawLabel(angle, true, bold, true, this.radial);
1205		}
1206	}
1207	
1208	this.drawHighlightCenter = function()
1209	{
1210		if ( ! this.canDisplayHistory() )
1211		{
1212			return;
1213		}
1214		
1215		context.lineWidth = highlightLineWidth;
1216		context.strokeStyle = 'black';
1217		context.fillStyle = "rgba(255, 255, 255, .6)";
1218		
1219		context.fillStyle = 'black';
1220		this.drawLabel(3 * Math.PI / 2, true, true, false);
1221		context.font = fontNormal;
1222	}
1223	
1224	this.drawKey = function(angle, highlight, bold)
1225	{
1226		var offset = keyOffset();
1227		var color;
1228		var patternAlpha = this.alphaPattern.end;
1229		var boxLeft = imageWidth - keySize - margin;
1230		var textY = offset + keySize / 2;
1231		
1232		var label;
1233		var keyNameWidth;
1234		
1235		if ( this == selectedNode )
1236		{
1237			color = colorUnclassified;
1238			label =
1239				this.getUnclassifiedText() +
1240				'   ' +
1241				this.getUnclassifiedPercentage();
1242			keyNameWidth = measureText(label, false);
1243		}
1244		else
1245		{
1246			label = this.keyLabel;
1247			color = rgbText(this.r.end, this.g.end, this.b.end);
1248			
1249			if ( highlight )
1250			{
1251				if ( this.searchResultChildren() )
1252				{
1253					label = label + searchResultString(this.searchResultChildren());
1254				}
1255				
1256				keyNameWidth = measureText(label, bold);
1257			}
1258			else
1259			{
1260				keyNameWidth = this.keyNameWidth;
1261			}
1262		}
1263		
1264		var textLeft = boxLeft - keyBuffer - keyNameWidth - fontSize / 2;
1265		var labelLeft = textLeft;
1266		
1267		if ( labelLeft > keyMinTextLeft - fontSize / 2 )
1268		{
1269			keyMinTextLeft -= fontSize / 2;
1270			
1271			if ( keyMinTextLeft < centerX - gRadius + fontSize / 2 )
1272			{
1273				keyMinTextLeft = centerX - gRadius + fontSize / 2;
1274			}
1275			
1276			labelLeft = keyMinTextLeft;
1277		}
1278		
1279		var lineX = new Array();
1280		var lineY = new Array();
1281		
1282		var bendRadius;
1283		var keyAngle = Math.atan((textY - centerY) / (labelLeft - centerX));
1284		var arcAngle;
1285		
1286		if ( keyAngle < 0 )
1287		{
1288			keyAngle += Math.PI;
1289		}
1290		
1291		if ( keyMinAngle == 0 || angle < keyMinAngle )
1292		{
1293			keyMinAngle = angle;
1294		}
1295		
1296		if ( angle > Math.PI && keyMinAngle > Math.PI )
1297		{
1298			// allow lines to come underneath the chart
1299			
1300			angle -= Math.PI * 2;
1301		}
1302		
1303		lineX.push(Math.cos(angle) * gRadius);
1304		lineY.push(Math.sin(angle) * gRadius);
1305		
1306		if ( angle < keyAngle && textY > centerY + Math.sin(angle) * (gRadius + buffer * (currentKey - 1) / (keys + 1) / 2 + buffer / 2) )
1307		{
1308			bendRadius = gRadius + buffer - buffer * currentKey / (keys + 1) / 2;
1309		}
1310		else
1311		{
1312			bendRadius = gRadius + buffer * currentKey / (keys + 1) / 2 + buffer / 2;
1313		}
1314		
1315		var outside =
1316			Math.sqrt
1317			(
1318				Math.pow(labelLeft - centerX, 2) +
1319				Math.pow(textY - centerY, 2)
1320			) > bendRadius;
1321		
1322		if ( ! outside )
1323		{
1324			arcAngle = Math.asin((textY - centerY) / bendRadius);
1325			
1326			keyMinTextLeft = min(keyMinTextLeft, centerX + bendRadius * Math.cos(arcAngle) - fontSize / 2);
1327			
1328			if ( labelLeft < textLeft && textLeft > centerX + bendRadius * Math.cos(arcAngle) )
1329			{
1330				lineX.push(textLeft - centerX);
1331				lineY.push(textY - centerY);
1332			}
1333		}
1334		else
1335		{
1336			keyMinTextLeft = min(keyMinTextLeft, labelLeft - fontSize / 2);
1337			
1338			if ( angle < keyAngle )
1339			{
1340				// flip everything over y = x
1341				//
1342				arcAngle = Math.PI / 2 - keyLineAngle
1343				(
1344					Math.PI / 2 - angle,
1345					Math.PI / 2 - keyAngle,
1346					bendRadius,
1347					textY - centerY,
1348					labelLeft - centerX,
1349					lineY,
1350					lineX
1351				);
1352				
1353			}
1354			else
1355			{
1356				arcAngle = keyLineAngle
1357				(
1358					angle,
1359					keyAngle,
1360					bendRadius,
1361					labelLeft - centerX,
1362					textY - centerY,
1363					lineX,
1364					lineY
1365				);
1366			}
1367		}
1368		
1369		if ( labelLeft > centerX + bendRadius * Math.cos(arcAngle) ||
1370		textY > centerY + bendRadius * Math.sin(arcAngle) + .01)
1371//		if ( outside ||  )
1372		{
1373			lineX.push(labelLeft - centerX);
1374			lineY.push(textY - centerY);
1375			
1376			if ( textLeft != labelLeft )
1377			{
1378				lineX.push(textLeft - centerX);
1379				lineY.push(textY - centerY);
1380			}
1381		}
1382		
1383		context.globalAlpha = this.alphaWedge.current();
1384		
1385		if ( snapshotMode )
1386		{
1387			var labelSVG;
1388			
1389			if ( this == selectedNode )
1390			{
1391				labelSVG =
1392					this.getUnclassifiedText() +
1393					spacer() +
1394					this.getUnclassifiedPercentage();
1395			}
1396			else
1397			{
1398				labelSVG = this.name + spacer() + this.getPercentage() + '%';
1399			}
1400			
1401			svg +=
1402				'<rect fill="' + color + '" ' +
1403				'x="' + boxLeft + '" y="' + offset +
1404				'" width="' + keySize + '" height="' + keySize + '"/>';
1405			
1406			if ( patternAlpha )
1407			{
1408				svg +=
1409					'<rect fill="url(#hiddenPattern)" style="stroke:none" ' +
1410					'x="' + boxLeft + '" y="' + offset +
1411					'" width="' + keySize + '" height="' + keySize + '"/>';
1412			}
1413			
1414			svg +=
1415				'<path class="line' +
1416				(highlight ? ' highlight' : '') +
1417				'" d="M ' + (lineX[0] + centerX) + ',' +
1418				(lineY[0] + centerY);
1419			
1420			if ( angle != arcAngle )
1421			{
1422				svg +=
1423					' L ' + (centerX + bendRadius * Math.cos(angle)) + ',' +
1424					(centerY + bendRadius * Math.sin(angle)) +
1425					' A ' + bendRadius + ',' + bendRadius + ' 0 ' +
1426					'0,' + (angle > arcAngle ? '0' : '1') + ' ' +
1427					(centerX + bendRadius * Math.cos(arcAngle)) + ',' +
1428					(centerY + bendRadius * Math.sin(arcAngle));
1429			}
1430			
1431			for ( var i = 1; i < lineX.length; i++ )
1432			{
1433				svg +=
1434					' L ' + (centerX + lineX[i]) + ',' +
1435					(centerY + lineY[i]);
1436			}
1437			
1438			svg += '"/>';
1439			
1440			if ( highlight )
1441			{
1442				if ( this.searchResultChildren() )
1443				{
1444					labelSVG = labelSVG + searchResultString(this.searchResultChildren());
1445				}
1446				
1447				drawBubbleSVG
1448				(
1449					boxLeft - keyBuffer - keyNameWidth - fontSize / 2,
1450					textY - fontSize,
1451					keyNameWidth + fontSize,
1452					fontSize * 2,
1453					fontSize,
1454					0
1455				);
1456				
1457				if ( this.isSearchResult )
1458				{
1459					drawSearchHighlights
1460					(
1461						label,
1462						boxLeft - keyBuffer - keyNameWidth,
1463						textY,
1464						0
1465					)
1466				}
1467			}
1468			
1469			svg += svgText(labelSVG, boxLeft - keyBuffer, textY, 'end', bold);
1470		}
1471		else
1472		{
1473			context.fillStyle = color;
1474			context.translate(-centerX, -centerY);
1475			context.strokeStyle = 'black';
1476				context.globalAlpha = 1;//this.alphaWedge.current();
1477			
1478			context.fillRect(boxLeft, offset, keySize, keySize);
1479			
1480			if ( patternAlpha )
1481			{
1482				context.globalAlpha = patternAlpha;
1483				context.fillStyle = hiddenPattern;
1484				
1485				// make clipping box for Firefox performance
1486				context.beginPath();
1487				context.moveTo(boxLeft, offset);
1488				context.lineTo(boxLeft + keySize, offset);
1489				context.lineTo(boxLeft + keySize, offset + keySize);
1490				context.lineTo(boxLeft, offset + keySize);
1491				context.closePath();
1492				context.save();
1493				context.clip();
1494				
1495				context.fillRect(boxLeft, offset, keySize, keySize);
1496				context.fillRect(boxLeft, offset, keySize, keySize);
1497				
1498				context.restore(); // remove clipping region
1499			}
1500			
1501			if ( highlight )
1502			{
1503				this.setHighlightStyle();
1504				context.fillRect(boxLeft, offset, keySize, keySize);
1505			}
1506			else
1507			{
1508				context.lineWidth = thinLineWidth;
1509			}
1510			
1511			context.strokeRect(boxLeft, offset, keySize, keySize);
1512			
1513			if ( lineX.length )
1514			{
1515				context.beginPath();
1516				context.moveTo(lineX[0] + centerX, lineY[0] + centerY);
1517				
1518				context.arc(centerX, centerY, bendRadius, angle, arcAngle, angle > arcAngle);
1519				
1520				for ( var i = 1; i < lineX.length; i++ )
1521				{
1522					context.lineTo(lineX[i] + centerX, lineY[i] + centerY);
1523				}
1524				
1525				context.globalAlpha = this == selectedNode ?
1526					this.children[0].alphaWedge.current() :
1527					this.alphaWedge.current();
1528				context.lineWidth = highlight ? highlightLineWidth : thinLineWidth;
1529				context.stroke();
1530				context.globalAlpha = 1;
1531			}
1532			
1533			if ( highlight )
1534			{
1535				drawBubbleCanvas
1536				(
1537					boxLeft - keyBuffer - keyNameWidth - fontSize / 2,
1538					textY - fontSize,
1539					keyNameWidth + fontSize,
1540					fontSize * 2,
1541					fontSize,
1542					0
1543				);
1544				
1545				if ( this.isSearchResult )
1546				{
1547					drawSearchHighlights
1548					(
1549						label,
1550						boxLeft - keyBuffer - keyNameWidth,
1551						textY,
1552						0
1553					)
1554				}
1555			}
1556			
1557			drawText(label, boxLeft - keyBuffer, offset + keySize / 2, 0, 'end', bold);
1558			
1559			context.translate(centerX, centerY);
1560		}
1561		
1562		currentKey++;
1563	}
1564	
1565	this.drawLabel = function(angle, bubble, bold, selected, radial)
1566	{
1567		if ( context.globalAlpha == 0 )
1568		{
1569			return;
1570		}
1571		
1572		var innerText;
1573		var label;
1574		var radius;
1575		
1576		if ( radial )
1577		{
1578			radius = (this.radiusInner.current() + 1) * gRadius / 2;
1579		}
1580		else
1581		{
1582			radius = this.labelRadius.current() * gRadius;
1583		}
1584		
1585		if ( radial && (selected || bubble ) )
1586		{
1587			var percentage = this.getPercentage();
1588			innerText = percentage + '%';
1589		}
1590		
1591		if
1592		(
1593			! radial &&
1594			this != selectedNode &&
1595			! bubble &&
1596			( !zoomOut || this != selectedNodeLast)
1597		)
1598		{
1599			label = this.shortenLabel();
1600		}
1601		else
1602		{
1603			label = this.name;
1604		}
1605		
1606		var flipped = drawTextPolar
1607		(
1608			label,
1609			innerText,
1610			angle,
1611			radius,
1612			radial,
1613			bubble,
1614			bold,
1615//			this.isSearchResult && this.shouldAddSearchResultsString() && (!selected || this == selectedNode || highlight),
1616			this.isSearchResult && (!selected || this == selectedNode || bubble),
1617			(this.hideAlone || !selected || this == selectedNode ) ? this.searchResultChildren() : 0
1618		);
1619		
1620		var depth = this.getDepth() - selectedNode.getDepth() + 1;
1621		
1622		if
1623		(
1624			! radial &&
1625			! bubble &&
1626			this != selectedNode &&
1627			this.angleEnd.end != this.angleStart.end &&
1628			nLabelOffsets[depth - 2] > 2 &&
1629			this.labelWidth.current() > (this.angleEnd.end - this.angleStart.end) * Math.abs(radius) &&
1630			! ( zoomOut && this == selectedNodeLast ) &&
1631			this.labelRadius.end > 0
1632		)
1633		{
1634			// name extends beyond wedge; draw tick mark towards the central
1635			// radius for easier identification
1636			
1637			var radiusCenter = compress ?
1638				(compressedRadii[depth - 1] + compressedRadii[depth - 2]) / 2 :
1639				(depth - .5) * nodeRadius;
1640			
1641			if ( this.labelRadius.end > radiusCenter )
1642			{
1643				if ( flipped )
1644				{
1645					drawTick(radius - tickLength * 1.4 , tickLength, angle);
1646				}
1647				else
1648				{
1649					drawTick(radius - tickLength * 1.7, tickLength, angle);
1650				}
1651			}
1652			else
1653			{
1654				if ( flipped )
1655				{
1656					drawTick(radius + tickLength * .7, tickLength, angle);
1657				}
1658				else
1659				{
1660					drawTick(radius + tickLength * .4, tickLength, angle);
1661				}
1662			}
1663		}
1664	}
1665	
1666	this.drawLines = function(angleStart, angleEnd, radiusInner, drawRadial, selected)
1667	{
1668		if ( snapshotMode )
1669		{
1670			if ( this != selectedNode)
1671			{
1672				if ( angleEnd == angleStart + Math.PI * 2 )
1673				{
1674					// fudge to prevent overlap, which causes arc ambiguity
1675					//
1676					angleEnd -= .1 / gRadius;
1677				}
1678				
1679				var longArc = angleEnd - angleStart > Math.PI ? 1 : 0;
1680				
1681				var x1 = centerX + radiusInner * Math.cos(angleStart);
1682				var y1 = centerY + radiusInner * Math.sin(angleStart);
1683				
1684				var x2 = centerX + gRadius * Math.cos(angleStart);
1685				var y2 = centerY + gRadius * Math.sin(angleStart);
1686				
1687				var x3 = centerX + gRadius * Math.cos(angleEnd);
1688				var y3 = centerY + gRadius * Math.sin(angleEnd);
1689				
1690				var x4 = centerX + radiusInner * Math.cos(angleEnd);
1691				var y4 = centerY + radiusInner * Math.sin(angleEnd);
1692				
1693				if ( this.alphaArc.end )
1694				{
1695					var dArray =
1696					[
1697						" M ", x4, ",", y4,
1698						" A ", radiusInner, ",", radiusInner, " 0 ", longArc,
1699							" 0 ", x1, ",", y1
1700					];
1701					
1702					svg += '<path class="line" d="' + dArray.join('') + '"/>';
1703				}
1704				
1705				if ( drawRadial && this.alphaLine.end )
1706				{
1707					svg += '<line x1="' + x3 + '" y1="' + y3 + '" x2="' + x4 + '" y2="' + y4 + '"/>';
1708				}
1709			}
1710		}
1711		else
1712		{
1713			context.lineWidth = thinLineWidth;
1714			context.strokeStyle = 'black';
1715			context.beginPath();
1716			context.arc(0, 0, radiusInner, angleStart, angleEnd, false);
1717			context.globalAlpha = this.alphaArc.current();
1718			context.stroke();
1719			
1720			if ( drawRadial )
1721			{
1722				var x1 = radiusInner * Math.cos(angleEnd);
1723				var y1 = radiusInner * Math.sin(angleEnd);
1724				var x2 = gRadius * Math.cos(angleEnd);
1725				var y2 = gRadius * Math.sin(angleEnd);
1726				
1727				context.beginPath();
1728				context.moveTo(x1, y1);
1729				context.lineTo(x2, y2);
1730				
1731//				if ( this.getCollapse() )//( selected && this != selectedNode )
1732				{
1733					context.globalAlpha = this.alphaLine.current();
1734				}
1735				
1736				context.stroke();
1737			}
1738		}
1739	}
1740	
1741	this.drawMap = function(child)
1742	{
1743		if ( this.parent )
1744		{
1745			this.parent.drawMap(child);
1746		}
1747		
1748		if ( this.getCollapse() && this != child || this == focusNode )
1749		{
1750			return;
1751		}
1752		
1753		var angleStart =
1754			(child.baseMagnitude - this.baseMagnitude) / this.magnitude * Math.PI * 2 +
1755			rotationOffset;
1756		var angleEnd =
1757			(child.baseMagnitude - this.baseMagnitude + child.magnitude) /
1758			this.magnitude * Math.PI * 2 +
1759			rotationOffset;
1760		
1761		var box = this.getMapPosition();
1762		
1763		context.save();
1764		context.fillStyle = 'black';
1765		context.textAlign = 'end';
1766		context.textBaseline = 'middle';
1767		
1768		var textX = box.x - mapRadius - mapBuffer;
1769		var percentage = getPercentage(child.magnitude / this.magnitude);
1770		
1771		var highlight = this == selectedNode || this == highlightedNode;
1772		
1773		if ( highlight )
1774		{
1775			context.font = fontBold;
1776		}
1777		else
1778		{
1779			context.font = fontNormal;
1780		}
1781		
1782		context.fillText(percentage + '% of', textX, box.y - mapRadius / 3);
1783		context.fillText(this.name, textX, box.y + mapRadius / 3);
1784		
1785		if ( highlight )
1786		{
1787			context.font = fontNormal;
1788		}
1789		
1790		if ( this == highlightedNode && this != selectedNode )
1791		{
1792			context.fillStyle = 'rgb(245, 245, 245)';
1793//			context.fillStyle = 'rgb(200, 200, 200)';
1794		}
1795		else
1796		{
1797			context.fillStyle = 'rgb(255, 255, 255)';
1798		}
1799		
1800		context.beginPath();
1801		context.arc(box.x, box.y, mapRadius, 0, Math.PI * 2, true);
1802		context.closePath();
1803		context.fill();
1804		
1805		if ( this == selectedNode )
1806		{
1807			context.lineWidth = 1;
1808			context.fillStyle = 'rgb(100, 100, 100)';
1809		}
1810		else
1811		{
1812			if ( this == highlightedNode )
1813			{
1814				context.lineWidth = .2;
1815				context.fillStyle = 'rgb(190, 190, 190)';
1816			}
1817			else
1818			{
1819				context.lineWidth = .2;
1820				context.fillStyle = 'rgb(200, 200, 200)';
1821			}
1822		}
1823		
1824		var maxDepth = this.getMaxDepth();
1825		
1826		if ( ! compress && maxDepth > maxPossibleDepth + this.getDepth() - 1 )
1827		{
1828			maxDepth = maxPossibleDepth + this.getDepth() - 1;
1829		}
1830		
1831		if ( this.getDepth() < selectedNode.getDepth() )
1832		{
1833			if ( child.getDepth() - 1 >= maxDepth )
1834			{
1835				maxDepth = child.getDepth();
1836			}
1837		}
1838		
1839		var radiusInner;
1840		
1841		if ( compress )
1842		{
1843			radiusInner = 0;
1844//				Math.atan(child.getDepth() - this.getDepth()) /
1845//				Math.PI * 2 * .9;
1846		}
1847		else
1848		{
1849			radiusInner =
1850				(child.getDepth() - this.getDepth()) /
1851				(maxDepth - this.getDepth() + 1);
1852		}
1853		
1854		context.stroke();
1855		context.beginPath();
1856		
1857		if ( radiusInner == 0 )
1858		{
1859			context.moveTo(box.x, box.y);
1860		}
1861		else
1862		{
1863			context.arc(box.x, box.y, mapRadius * radiusInner, angleEnd, angleStart, true);
1864		}
1865		
1866		context.arc(box.x, box.y, mapRadius, angleStart, angleEnd, false);
1867		context.closePath();
1868		context.fill();
1869		
1870		if ( this == highlightedNode && this != selectedNode )
1871		{
1872			context.lineWidth = 1;
1873			context.stroke();
1874		}
1875		
1876		context.restore();
1877	}
1878	
1879	this.drawReferenceRings = function(childRadiusInner)
1880	{
1881		if ( snapshotMode )
1882		{
1883			svg +=
1884				'<circle cx="' + centerX + '" cy="' + centerY +
1885				'" r="' + childRadiusInner + '"/>';
1886			svg +=
1887				'<circle cx="' + centerX + '" cy="' + centerY +
1888				'" r="' + gRadius + '"/>';
1889		}
1890		else
1891		{
1892			context.globalAlpha = 1 - this.alphaLine.current();//this.getUncollapsed().alphaLine.current();
1893			context.beginPath();
1894			context.arc(0, 0, childRadiusInner, 0, Math.PI * 2, false);
1895			context.stroke();
1896			context.beginPath();
1897			context.arc(0, 0, gRadius, 0, Math.PI * 2, false);
1898			context.stroke();
1899		}
1900	}
1901	
1902	this.getCollapse = function()
1903	{
1904		return (
1905			collapse &&
1906			this.collapse &&
1907			this.depth != maxAbsoluteDepth
1908			);
1909	}
1910	
1911	this.getDepth = function()
1912	{
1913		if ( collapse )
1914		{
1915			return this.depthCollapsed;
1916		}
1917		else
1918		{
1919			return this.depth;
1920		}
1921	}
1922	
1923	this.getMagnitude = function()
1924	{
1925		return this.attributes[magnitudeIndex][currentDataset];
1926	}
1927	
1928	this.getMapPosition = function()
1929	{
1930		return {
1931			x : (details.offsetLeft + details.clientWidth - mapRadius),
1932			y : ((focusNode.getDepth() - this.getDepth()) *
1933				(mapBuffer + mapRadius * 2) - mapRadius) +
1934				details.clientHeight + details.offsetTop
1935		};
1936	}
1937	
1938	this.getMaxDepth = function(limit)
1939	{
1940		var max;
1941		
1942		if ( collapse )
1943		{
1944			return this.maxDepthCollapsed;
1945		}
1946		else
1947		{
1948			if ( this.maxDepth > maxAbsoluteDepth )
1949			{
1950				return maxAbsoluteDepth;
1951			}
1952			else
1953			{
1954				return this.maxDepth;
1955			}
1956		}
1957	}
1958	
1959	this.getData = function(index, summary)
1960	{
1961		var files = new Array();
1962		
1963		if
1964		(
1965			this.attributes[index] != null &&
1966			this.attributes[index][currentDataset] != null &&
1967			this.attributes[index][currentDataset] != ''
1968		)
1969		{
1970			files.push
1971			(
1972				document.location +
1973				'.files/' +
1974				this.attributes[index][currentDataset]
1975			);
1976		}
1977		
1978		if ( summary )
1979		{
1980			for ( var i = 0; i < this.children.length; i++ )
1981			{
1982				files = files.concat(this.children[i].getData(index, true));
1983			}
1984		}
1985		
1986		return files;
1987	}
1988	
1989	this.getList = function(index, summary)
1990	{
1991		var list;
1992		
1993		if
1994		(
1995			this.attributes[index] != null &&
1996			this.attributes[index][currentDataset] != null
1997		)
1998		{
1999			list = this.attributes[index][currentDataset];
2000		}
2001		else
2002		{
2003			list = new Array();
2004		}
2005		
2006		if ( summary )
2007		{
2008			for ( var i = 0; i < this.children.length; i++ )
2009			{
2010				list = list.concat(this.children[i].getList(index, true));
2011			}
2012		}
2013		
2014		return list;
2015	}
2016	
2017	this.getParent = function()
2018	{
2019		// returns parent, accounting for collapsing or 0 if doesn't exist
2020		
2021		var parent = this.parent;
2022		
2023		while ( parent != 0 && parent.getCollapse() )
2024		{
2025			parent = parent.parent;
2026		}
2027		
2028		return parent;
2029	}
2030	
2031	this.getPercentage = function()
2032	{
2033		return getPercentage(this.magnitude / selectedNode.magnitude);
2034	}
2035	
2036	this.getUnclassifiedPercentage = function()
2037	{
2038		var lastChild = this.children[this.children.length - 1];
2039		
2040		return getPercentage
2041		(
2042			(
2043				this.baseMagnitude +
2044				this.magnitude -
2045				lastChild.magnitude -
2046				lastChild.baseMagnitude
2047			) / this.magnitude
2048		) + '%';
2049	}
2050	
2051	this.getUnclassifiedText = function()
2052	{
2053		return '[unassigned '+ this.name + ']';
2054	}
2055	
2056	this.getUncollapsed = function()
2057	{
2058		// recurse through collapsed children until uncollapsed node is found
2059		
2060		if ( this.getCollapse() )
2061		{
2062			return this.children[0].getUncollapsed();
2063		}
2064		else
2065		{
2066			return this;
2067		}
2068	}
2069	
2070	this.hasChildren = function()
2071	{
2072		return this.children.length && this.depth < maxAbsoluteDepth && this.magnitude;
2073	}
2074	
2075	this.hasParent = function(parent)
2076	{
2077		if ( this.parent )
2078		{
2079			if ( this.parent == parent )
2080			{
2081				return true;
2082			}
2083			else
2084			{
2085				return this.parent.hasParent(parent);
2086			}
2087		}
2088		else
2089		{
2090			return false;
2091		}
2092	}
2093	
2094	this.maxVisibleDepth = function(maxDepth)
2095	{
2096		var childInnerRadius;
2097		var depth = this.getDepth() - selectedNode.getDepth() + 1;
2098		var currentMaxDepth = depth;
2099		
2100		if ( this.hasChildren() && depth < maxDepth)
2101		{
2102			var lastChild = this.children[this.children.length - 1];
2103			
2104			if ( this.name == 'Pseudomonadaceae' )
2105			{
2106				var x = 3;
2107			}
2108			
2109			if
2110			(
2111				lastChild.baseMagnitude + lastChild.magnitude <
2112				this.baseMagnitude + this.magnitude
2113			)
2114			{
2115				currentMaxDepth++;
2116			}
2117			
2118			if ( compress )
2119			{
2120				childInnerRadius = compressedRadii[depth - 1];
2121			}
2122			else
2123			{
2124				childInnerRadius = (depth) / maxDepth;
2125			}
2126			
2127			for ( var i = 0; i < this.children.length; i++ )
2128			{
2129				if
2130				(//true ||
2131					this.children[i].magnitude *
2132					angleFactor *
2133					(childInnerRadius + 1) *
2134					gRadius >=
2135					minWidth()
2136				)
2137				{
2138					var childMaxDepth = this.children[i].maxVisibleDepth(maxDepth);
2139					
2140					if ( childMaxDepth > currentMaxDepth )
2141					{
2142						currentMaxDepth = childMaxDepth;
2143					}
2144				}
2145			}
2146		}
2147		
2148		return currentMaxDepth;
2149	}
2150	
2151	this.resetLabelWidth = function()
2152	{
2153		var nameWidthOld = this.nameWidth;
2154		
2155		if ( ! this.radial )//&& fontSize != fontSizeLast )
2156		{
2157			var dim = context.measureText(this.name);
2158			this.nameWidth = dim.width;
2159		}
2160		
2161		if ( fontSize != fontSizeLast && this.labelWidth.end == nameWidthOld * labelWidthFudge )
2162		{
2163			// font size changed; adjust start of tween to match
2164			
2165			this.labelWidth.start = this.nameWidth * labelWidthFudge;
2166		}
2167		else
2168		{
2169			this.labelWidth.start = this.labelWidth.current();
2170		}
2171		
2172		this.labelWidth.end = this.nameWidth * labelWidthFudge;
2173	}
2174	
2175	this.restrictLabelWidth = function(width)
2176	{
2177		if ( width < this.labelWidth.end )
2178		{
2179			this.labelWidth.end = width;
2180		}
2181	}
2182	
2183	this.search = function()
2184	{
2185		this.isSearchResult = false;
2186		this.searchResults = 0;
2187		
2188		if
2189		(
2190			! this.getCollapse() &&
2191			search.value != '' &&
2192			this.name.toLowerCase().indexOf(search.value.toLowerCase()) != -1
2193		)
2194		{
2195			this.isSearchResult = true;
2196			this.searchResults = 1;
2197			nSearchResults++;
2198		}
2199		
2200		for ( var i = 0; i < this.children.length; i++ )
2201		{
2202			this.searchResults += this.children[i].search();
2203		}
2204		
2205		return this.searchResults;
2206	}
2207	
2208	this.searchResultChildren = function()
2209	{
2210		if ( this.isSearchResult )
2211		{
2212			return this.searchResults - 1;
2213		}
2214		else
2215		{
2216			return this.searchResults;
2217		}
2218	}
2219	
2220	this.setDepth = function(depth, depthCollapsed)
2221	{
2222		this.depth = depth;
2223		this.depthCollapsed = depthCollapsed;
2224		
2225		if
2226		(
2227			this.children.length == 1 &&
2228//			this.magnitude > 0 &&
2229			this.children[0].magnitude == this.magnitude &&
2230			( head.children.length > 1 || this.children[0].children.length )
2231		)
2232		{
2233			this.collapse = true;
2234		}
2235		else
2236		{
2237			this.collapse = false;
2238			depthCollapsed++;
2239		}
2240		
2241		for ( var i = 0; i < this.children.length; i++ )
2242		{
2243			this.children[i].setDepth(depth + 1, depthCollapsed);
2244		}
2245	}
2246	
2247	this.setHighlightStyle = function()
2248	{
2249		context.lineWidth = highlightLineWidth;
2250		
2251		if ( this.hasChi

Large files files are truncated, but you can click here to view the full file