//JavaScript Document
/**
* The Dendrogrammer - production version 1.0.0
*
* The Dendrogrammer creates an interactive dendrogram from clustering data.
* These pages document the JavaScript part of the application (which is the major part).
* <br /><br />
* The Dendrogrammer was created by David Allan Robb, March 2011 to August 2011<br /><br />
*
* NOTE: IMPORTANT: You need to know: There is a limit placed on the number of
* clusters that can be grown into the cluster tree. It is set in the ClusterNode
* constructor, this.growLimit=2000. It uses a global variable, growCounter. It is an
* arbitrary limit placed to prevent interminable loops with erroneous data. It could
* be increased.<br /><br />
*
*
* There are some 'constants' declared at the start which could be tuned to affect the
* dendrogram appearance. These are: <br />
* -leafSpace (the amount of vertical space occupied on the chart by one leaf),<br />
* -margin (the amount of space around the chart outside the axes), and <br />
* -leafLabelSpace (the amount of space for the labels down the vertical axis).<br /><br />
*
* There are also three element styling 'constants': <br />
* standardStrokeWidth,<br />
* standardLeafRadius,<br /> and nodeHighlightColour.<br /><br />
*
* The following instances of class objects are declared at a global level. (However,
* none of these objects are accessed via global scope. If one of the classes requires access
* to another it is passed a reference as a parameter.) Refer to class descriptions for
* details:<br />
* -theClusterTable, the instance of ClusterTable <br />
* -theStyles, the instance of Styles<br />
* -theClusterTree, the instance of ClusterTree<br />
* -theCanvas, the instance of Canvas<br />
* -theAxes, the instance of GraphAxes<br />
* -theDendrogram, the instance of Dendrogram
*
* @Module dendrogrammer
*
*/
$(document).ready( function() {
//##########################################################################
//##########################################################################
//PROCESSING STARTS HERE
//##########################################################################
//##########################################################################
//Constants. Placed here to allow easy location for tuning
//to help dimension the Raphael canvas
var leafSpace=8; var margin=45; var leafLabelSpace=50;
//standard styles for elements
var standardStrokeWidth= 1.5;
var standardLeafRadius= 2;
var nodeHighlightColour="red";
//END OF Constants.
//###########################################################################
//###########################################################################
//instantiate the Gui class for user interaction
//###########################################################################
var theGui=new Gui();
//#######################################################################
//The data structure
//ClusterTable made of read in
// - clusterRows (node data),
// - datRows (filename data or manifest)
// - and leafRows (leaf description data)
//and
//ClusterTree made of ClusterNodes (made from the ClusterTable data).
//######################################################################
//set up the ClusterTable reading in the data
var theClusterTable= new ClusterTable();
//Global count of sprouted leaf nodes. Used when creating the tree
//incremented by the global function, getNextLeafSproutNo()
var leafSproutCounter=0;
var growCounter=0;// used in limiting the growth (see top of code).
//create the Styles object
var theStyles= new Styles(standardStrokeWidth,standardLeafRadius,
nodeHighlightColour);
//set up the ClusterTree using data from the ClusterTable
var theClusterTree= new ClusterTree(theClusterTable,theStyles);
//set up the canvas
var theCanvas = new Canvas(theClusterTree,theClusterTable,margin,
leafSpace,leafLabelSpace, theGui);
//draw the Axes using the GraphAxes class
var theAxes= new GraphAxes(theCanvas,0,true);//x axis from 0 and have notches
//###########################################################################
//draw the dendrogram
var theDendrogram = new Dendrogram(theClusterTree,theCanvas,theStyles,theGui,theAxes);
//#############################################################################
//Bind function to buttons
theGui.bindButtonFunctions(theDendrogram,theGui);
//###########################################################################
//output values to web page
theGui.showDataDisplay(theCanvas.largestHt,theCanvas.noOfLeafs);
//End OF output values to web page
//############################################################################
//##########################################################################
//PROCESSING ENDS HERE
//##########################################################################
//##########################################################################
//###########################################################################
//Global Functions and objects
//###########################################################################
/*
* A global method to access and increment the leafSproutCounter
* @method getNextLeafSproutNo
* increments the global leafSproutCounter
* @return the value of that counter
*/
function getNextLeafSproutNo(){
//increment count
leafSproutCounter++;
return leafSproutCounter;
}
/*
* A global object constructor to create point objects
* @constructor point
* Point object constructor
* @return an object with x and y properties as per parameters
*/
function point(x,y){
this.x=x;
this.y=y;
}
/*
* For iPad detection.
* ref: Nicholas C. Zakas
* http://www.nczonline.net/blog/2010/04/06/ipad-web-development-tips/
* This is not made use of now. However I have left it in in case it may be
* needed in future. Instead a more generic mobile OS detection is done elsewhere
* @method isIPad
* @return boolean
*/
function isIPad(){
return navigator.platform == "iPad";
}
//END OF Global Functions and objects
//###########################################################################
//###########################################################################
//Class Definitions
//###########################################################################
//Below here are all the class definitions.
//######################################################
//GraphAxes class
/**
* For defining, drawing, and redrawing the dendrogram axes<br /><br />
*
* The constructor creates x and y axis graphic and axis label objects on the given
* Raphael canvas.
*
* @class GraphAxes
*/
//Define functions for the methods
/**
* Styles a given axis
* @method style
* @param axis {graphic object} the axis object to be styled
* @param title {string} title text for the axis mouse-over
* @return {boolean} true. Return value not used.
*/
function GraphAxes_style(axis,title){
//use the attr method to style the axis object
axis.attr(
{
stroke: '#b2b2b2',
'stroke-width': 2,
title: title
});
return true;//not used
}
/**
* Draws an horizontal axis with notches every so often according to parameters
* @method drawXAxis
* @param paper {Raphael canvas}
* @param startPt {point object} canvas point to start drawing
* @param length {number} length to be drawn
* @param notchInterval {number} interval at which to place notches
* @return {graphic element} the drawn X axis
*/
function GraphAxes_drawXAxis(paper, startPt, length, notchInterval ){
var notchSize=3;
var myPathString="M "+startPt.x+" "+startPt.y+" ";
currentLength=length;
currentX=startPt.x;
while ((currentLength-notchInterval)>=0){
myPathString+="l 0 -"+notchSize+" l 0 "+notchSize;//add a notch
//draw an interval
myPathString+="l"+notchInterval+" 0";
//chop interval off length
currentLength=currentLength-notchInterval;
}
//one more notch
myPathString+="l 0 -"+notchSize+" l 0 "+notchSize;//add a notch
//complete the length
myPathString+="l "+currentLength+" 0 z";
return paper.path(myPathString);
}
/**
* Draws a smooth horizontal axis with no notches except at start and end
* @method drawXAxisSmooth
*
* @param paper {Raphael canvas}
* @param startPt {Point}
* @param length {number}
* @return {graphic element} the drawn X axis
*/
function GraphAxes_drawXAxisSmooth(paper, startPt, length ){
var notchSize=3;
var myPathString="M "+startPt.x+" "+startPt.y+" ";
myPathString+="l 0 -"+notchSize+" l 0 "+notchSize;//add a notch
myPathString+="l "+length+" 0 ";
myPathString+="l 0 -"+notchSize+" l 0 "+notchSize;//add a final notch
myPathString+="z";//end it
return paper.path(myPathString);
}
/**
* Removes drawn elements of the axes.
* @method remove
*/
function GraphAxes_remove(){
this.xAxis.remove();
this.yAxis.remove();
this.xAxisLabel.remove();
this.xAxisLabelB.remove();
this.originLabel.remove();
}
/**
* Creates x and y axis graphic objects on the given Raphael canvas
* @constructor GraphAxes
* @param canvasIn - the Canvas ( a Canvas object)
* @param originHtIn - the Ht at the origin (a number)
* @param isNormalGraph - indidates if axes are for normal or summary.
* Affects labelling whether or not the X axis is to have notches (boolean)
* @return {GraphAxes}
*/
//Constructor function
function GraphAxes(canvasIn, originHtIn, isNormalGraph ){
//initialise attributes
//none read straight in
//associate methods
this.style = GraphAxes_style;
this.drawXAxis= GraphAxes_drawXAxis;
this.drawXAxisSmooth= GraphAxes_drawXAxisSmooth;
this.remove = GraphAxes_remove;
//derived attributes
// set up origin.x and origin.y
this.origin=new point(canvasIn.margin+canvasIn.leafLabelSpace,canvasIn.margin);
if (isNormalGraph){//draw an x-axis with notches
this.xAxis=this.drawXAxis(canvasIn.paper, this.origin,
canvasIn.xWidth-(canvasIn.margin*2)-canvasIn.leafLabelSpace,
canvasIn.htScaleFactor/2);
}else{//draw a smooth x-axis
this.xAxis=this.drawXAxisSmooth(canvasIn.paper, this.origin,
canvasIn.xWidth-(canvasIn.margin*2)-canvasIn.leafLabelSpace);
}
this.yAxis= canvasIn.paper.path("M "+this.origin.x+" "+this.origin.y+"l 0 "
+(canvasIn.yHt-(canvasIn.margin*2)));
//label the axes
//create some drawn text to show the max value of ht. Call it xAxisLabel.
this.xAxisLabel = canvasIn.paper.text(
(canvasIn.largestHt*canvasIn.htScaleFactor+canvasIn.margin
+canvasIn.leafLabelSpace),
canvasIn.margin-10,
'Max ht = '+canvasIn.largestHt);
this.xAxisLabelB = canvasIn.paper.text(
((canvasIn.largestHt*canvasIn.htScaleFactor/2)+canvasIn.margin
+canvasIn.leafLabelSpace),
canvasIn.margin-20,
'Dissimilarity');
//label the origin
this.originLabel = canvasIn.paper.text((canvasIn.margin
+canvasIn.leafLabelSpace), canvasIn.margin-10, originHtIn);
//if this is a summary graph then label the leaf axis with "Summary"
if(originHtIn>0){
this.summaryLeafLabel= canvasIn.paper.text(canvasIn.margin,
Math.round(canvasIn.yHt/2),
'Summary.\nClick\nclusters for\nleaf lists.');
}
//actions to be done by the constructor
this.style(this.xAxis,"X-axis");//style the xAxis
this.style(this.yAxis,"Y-axis");//style the yAxis
}
//End of GraphAxes class
//######################################################
//######################################################
//ClusterTable class
/**
* Reads the data (held in the page as hidden elements of an HTML form) into a table.
* The 'table' is
* actually three tables: an array of clusterRows, an array of datRows, and an array
* of nodeRows.<br /><br />
*
* The constructor calls the getData() method to fill the arrays. It also
* calls assignIdNos() method.<br /><br />
*
* ClusterTree is generated from the ClusterTable and once this has been done
* the job of the ClusterTable is complete and it is not referred to again.
*
* @class ClusterTable
*/
//Define functions for the methods
/**
* Fills the table with data.<br />
* Gathers data from the hidden form elements and insert in the table<br />
* 1) Creates an associative array of all the input elements in the page<br />
* 2) Dat file data: loop through the array picking out the fields for each Dat row
* (manifest file)<br />
* 3) Node file data: as above but for the main cluster data <br />
* 4) Leaf file data: as above but for the leaf data <br /><br />
*
* Encompasses object constructor functions for clusterRow, datRow and leafRow
* objects.
*
* @method getData
*
* @return {string} the filename of the originating Dat file.
*/
function ClusterTable_getData(){//fills the table with data
//object constructor
function clusterRow(itemA, itemB, mergeHt, redrawn){
this.itemA=itemA;
this.itemB=itemB;
this.mergeHt=mergeHt;
this.redrawn=redrawn;
this.idNo=0;//set to zero for now
}
//object constructor
function datRow(fileNameIn){
this.fileName=fileNameIn;
}
//object constructor
function leafRow(shortIn, longIn){
this.shortDescription=shortIn;
this.longDescription=longIn;
}
//gather data from the hidden form elements and insert in the table
//create an associative array of all the input elements in the page
var $allInputElements=$(':input');//gather input elements in a jQuery object
//make an array
var inputValues = {};
$allInputElements.each(function() {
inputValues[this.name] = $(this).val();
});
//extract the file name
var fileName=inputValues['fileName'];
$( "#traceDump" ).append("fileName="+fileName+"</br>");
//First the dat file data
//loop through the array picking out the fields for each row
var row=1;
field = "dat"+row+"col0";
//$( "#traceDump" ).append("!value="+inputValues[field]+"!");
while (inputValues[field]!=null){
fileNameIn=inputValues[field];
this.datArray[row-1]=new datRow(fileNameIn);
//$( "#traceDump" ).append("!Read into datArray row"+(row-1)+"!");
row++;//next row
field="dat"+row+"col0";
}
//$( "#traceDump" ).append("!Finished loop!</br>");
//Second the node file data
//loop through the array picking out the fields for each row
row=1;
field = "node"+row+"col0";
//$( "#traceDump" ).append("!value="+inputValues[field]+"!");
while (inputValues[field]!=null){
itemA=inputValues[field];
field="node"+row+"col1";
itemB=inputValues[field];
field="node"+row+"col2";
mergeHt=inputValues[field];
this.cArray[row-1]=new clusterRow(itemA, itemB, mergeHt, false);
//$( "#traceDump" ).append("!Read into cArray row"+(row-1)+"!");
row++;//next row
field="node"+row+"col0";
}
//$( "#traceDump" ).append("!Finished loop!</br>");
//Third the leaf file data
//loop through the array picking out the fields for each row
var row=1;
field = "leaf"+row+"col0";
//$( "#traceDump" ).append("!value="+inputValues[field]+"!");
while (inputValues[field]!=null){
shortDesc=inputValues[field];
field="leaf"+row+"col1";
longDesc=inputValues[field];
this.leafArray[row-1]=new leafRow(shortDesc, longDesc);
//$( "#traceDump" ).append("!Read into leafArray row"+(row-1)+"!");
row++;//next row
field="leaf"+row+"col0";
}
//$( "#traceDump" ).append("!Finished loop!</br>");
return fileName;// returns the name of the file originating the data
}
/**
* Returns a row from the cArray by index
* @method getRowByIndex
*
* @param indexIn {number}
* @return {clusterRow object}
*/
function ClusterTable_getRowByIndex(indexIn){
return this.cArray[indexIn];
}
/**
* Returns the row indexed by the current pointer value
* @method getCurrentRow
*
* @return {clusterRow object}
*/
function ClusterTable_getCurrentRow(){
return this.cArray[this.pointer];
}
/**
* Returns the current pointer value
* @method getPtr
*
* @return {integer}
*/
function ClusterTable_getPtr(){
return this.pointer;
}
/**
* Increments the pointer then returns the newly pointed to row.
* Should only be called after checking with .nextRowExists
*
* @method getNextRow
*
* @return {clusterRow object}
*/
function ClusterTable_getNextRow(){
this.pointer++;
return this.cArray[this.pointer];
}
/**
* Returns the last row in the table
*
* @method getLast
*
* @return {clusterRow object}
*/
function ClusterTable_getLast(){
return this.cArray[this.cArray.length-1];
}
/**
* Detects whether or not there is another row beyond that pointed to by the pointer
*
* @method nextRowExists
*
* @return {boolean}
*/
function ClusterTable_nextRowExists(){
if(this.pointer+1<this.cArray.length){
return true;
}
return false;
}
/**
* Loops through the rows assigning an ID to the clusters
* the first is lastLeaf number +1 the next is lastLeaf+2 and so on
*
* @method assignIdNos
*/
function ClusterTable_assignIdNos(){
nextClusterNo=this.lastDataRow+3;//e.g. 0 to 4 rows = 5 data rows = 6 leafs
//= the first cluster is number 7
for (i=0;i<this.cArray.length;i++){
this.cArray[i].idNo=nextClusterNo;
nextClusterNo++;
}
}
/**
* Detects whether a cluster IdNo represents a leaf.
* It will be a leaf if the idNo <= the number of leaves
*
* @method isLeafByIdNo
*
* @param idNoIn {integer}
*
* @return {boolean}
*/
function ClusterTable_isLeafByIdNo(idNoIn){
if(idNoIn<=this.getNoOfLeaves()&&idNoIn>0){
return true;
}
return false;
}
/**
* Loops through the rows for the given IdNo and returns the array index for that row
*
* @method getRowIndexByIdNo
*
* @param idNoIn {integer}
*
* @return {integer}the array index for that row
*/
function ClusterTable_getRowIndexByIdNo(IdNoIn){
for (i=0;i<=this.lastDataRow;i++){
if(this.getRowByIndex(i).IdNo==IdNoIn){
return i;
}
}
return false;
}
/**
* Loops through the rows for the given IdNo and returns the itemA for that row
*
* @method getItemAByIdNo
*
* @param idNoIn {integer}
*
* @return {number} the itemA for that row
*/
function ClusterTable_getItemAByIdNo(idNoIn){
for (i=0;i<this.cArray.length;i++){
if(this.getRowByIndex(i).idNo==idNoIn){
return this.getRowByIndex(i).itemA;
}
}
return false;
}
/**
* Loops through the rows for the given IdNo and returns the itemB for that row
*
* @method getItemBByIdNo
*
* @param idNoIn {integer}
*
* @return {number} the itemB for that row
*/
function ClusterTable_getItemBByIdNo(idNoIn){
for (i=0;i<=this.lastDataRow;i++){
if(this.getRowByIndex(i).idNo==idNoIn){
return this.getRowByIndex(i).itemB;
}
}
return false;
}
/**
* Loops through the rows for the given IdNo and returns
* the mergeHt for that row with that given IdNo
*
* @method getHtByIdNo
*
* @param idNoIn {integer}
*
* @return {number} the itemB for that row
*/
function ClusterTable_getHtByIdNo(idNoIn){
//returns the mergeHt for that row with that given IdNo
//if it is a leaf id then return zero a leaf's ht is zero
if(this.isLeafByIdNo(idNoIn)){
return 0;
}
//loops through the rows for the given IdNo and returns the mergeHt for that row
for (i=0;i<=this.lastDataRow;i++){
if(this.getRowByIndex(i).idNo==idNoIn){
return this.getRowByIndex(i).mergeHt;
}
}
return false;
}
/**
* Returns the number of rows in the cArray table
*
* @method getNoOfLeaves
*
* @param idNoIn {integer}
*
* @return {number} the itemB for that row
*/
function ClusterTable_getNoOfLeaves(){
return this.cArray.length+1;
}
//Constructor function
/**
* Calls the getData() method to fill the tables. Calls assignIdNos() method.
* @constructor ClusterTable
*/
function ClusterTable(){
//initialise attributes
this.cArray= new Array();//to hold the table rows
this.datArray= new Array();//to hold the dat table rows
this.leafArray= new Array();//to hold the leaf table rows
//set the array pointer to -1 to indicate empty dendrogram
this.pointer=-1;
//associate methods
this.getData = ClusterTable_getData;
this.getRowByIndex = ClusterTable_getRowByIndex;
this.getCurrentRow = ClusterTable_getCurrentRow;
this.getPtr = ClusterTable_getPtr;
this.getNextRow = ClusterTable_getNextRow;
this.nextRowExists = ClusterTable_nextRowExists;
this.assignIdNos = ClusterTable_assignIdNos;
this.getLast = ClusterTable_getLast;
this.getNoOfLeaves= ClusterTable_getNoOfLeaves;
this.getRowIndexByIdNo= ClusterTable_getRowIndexByIdNo;
this.getItemAByIdNo= ClusterTable_getItemAByIdNo;
this.getItemBByIdNo= ClusterTable_getItemBByIdNo;
this.isLeafByIdNo= ClusterTable_isLeafByIdNo;
this.getHtByIdNo= ClusterTable_getHtByIdNo;
//actions to be done by the Constructor
this.getData();//fill the table
this.lastDataRow=this.cArray.length-1;
this.assignIdNos();
}
//End of ClusterTable class
//######################################################
//######################################################
//ClusterTree class
/**
* The ClusterTree class holds a tree of clusters. Actually it is one root node leading
* to all the other nodes in the tree.
* The tree is a single root node which is the overall ancestor of all nodes in the
* tree. Each node is an instance of the ClusterNode class.
* Each node represents a cluster from the ClusterTable.
* The root node is the cluster that has the highest merge height.<br />
* Each node has<br />
* - nType: either leaf 2, cluster 1, or root 0<br />
* - childA: one of the two child nodes (that are other node objects)<br />
* - childB: see above<br />
* - xHt: the height of the cluster OR zero for a leaf<br />
* - parent: a parent node (another node object)<br />
* - yPos: for a leaf this is the numbered leaf slot on the leaf axis (y axis).
* For a cluster or the root this is the point midway (in y) between the yPos
* of its two child nodes.<br />
* For the purpose of the algorithm the leaf axis is the y axis because the
* default orientation for the tree is on its side with root at the right
* and leaves at the left arranged down the y axis.<br />
* - element: a visible drawn graphical object representing the cluster on the graph<br />
* - bBox: a bounding box, a transparent graphical object representing the live area
* on the graph associated with the cluster<br />
* - rowIndex: the index of the row on the clusterTable from which it was generated<br />
* - idNo: the number given it by MATLAB in creating the clustering data<br />
* e.g. in our example data the cluster on row 0 (Item A =1, ItemB=6 which are
* two leaves are part of the first cluster. 5 rows (0 to 4) => 6 leafs => first cluster
* is no.7 so the idNo for that cluster will be 7.
* <br /><br />
* The ClsterTree constructor calls the new ClusterNode constructor method on the root
* node which in turn calls the ClusterNode grow() method which recursively calls
* itself to build the tree from the
* data in ClusterTable table. The constructor also sets the graph yPos for each node.
* <br /><br />
* Important ClusterTree attribute: <br />
* this.root. However, this.root is accessed via the getRoot() method.
*
* @class ClusterTree
*/
//Define functions for the methods
/**
* @method getRoot
* @return {node} the root node in the tree
*/
function ClusterTree_getRoot(){
return this.root;
}
/**
* Recursively traverses the tree setting the yPos attribute.
* Based on the yPos of the leaf nodes.
* The yPos for each leaf is already set (it is the sproutCount property)
* The yPos for a cluster is half way between the childA.yPos and childB.yPos
* but the yPos of the clusters start off null.
*
* @method setClusterY
* @param node {a node object}
* @return {boolean} value not used.
*/
function ClusterTree_setClusterY(node){
//if the yPos if childA is null then set its yPos
//if the yPos of childB is null then set its ypos
if (node.childA.yPos==null){
this.setClusterY(node.childA);
}
if(node.childB.yPos==null){
this.setClusterY(node.childB);
}
//finally set the yPos based on yPoses of children
if (node.childA.yPos<node.childB.yPos){
//set ypos to that of child A + difference between A and B
node.yPos=node.childA.yPos+(Math.abs(node.childA.yPos-node.childB.yPos)/2);
}else{
//set ypos to that of child B + difference between A and B
node.yPos=node.childB.yPos+(Math.abs(node.childA.yPos-node.childB.yPos)/2);
}
return true;
}
/**
* Calls the new ClusterNode constructor method which in turn calls the CLusterNode
* grow() method which recursively calls itself to build the from the
* data in ClusterTable table. It also sets the graph yPos for each cluster.
*
* @constructor ClusterTree
* @param cTableIn {ClusterTable} the instance of ClusterTable containing the input data.
* @param stylesIn {style object} carries the style info for local access
*/
function ClusterTree(cTableIn,stylesIn){
//initialise attributes
this.cTable=cTableIn;
this.styles=stylesIn;
//the root node is the node from the end of the clusterArray
//i.e. that cluster with the highest cluster height
var rootIdNo=this.cTable.getLast().idNo;
//This line causes a recursive all on the CusterNode Constructor
//in conjunction with the data in the cluster table which fill up
//the tree
this.root= new ClusterNode(this.cTable,rootIdNo,null,0,this.styles);
//associate methods
this.getRoot = ClusterTree_getRoot;
this.setClusterY = ClusterTree_setClusterY;
//processing to be done by the constructor
//Set the yPos values for all the clusters
this.setClusterY(this.root);
}
//End of ClusterTree class
//######################################################
//######################################################
//ClusterNode class
/**
* The ClusterTree is made up of ClusterNodes. See description of ClusterTree class
* for further details. A ClusterTree is a tree structure of these ClusterNodes.
* <br /><br />
*
* The constructor does the following:<br />
* If this node is a leaf <br />
* then it sets the child nodes to null and set default leaf descriptions
* and that is the end of that.<br />
* else it calls grow(), causing recursive calls on the new Cluster Node constructor
* to spawn its childA and its childB child nodes.
* (and also sets default cluster descriptions)<br /><br />
*
* Important ClusterNode attributes used by other classes:<br /><br />
*
* parent - ancestor node or null for root.
* Used in traversing the ClusterTree data structure as are childA and B<br />
* childA - descendant node or null for leaf<br />
* childB - as childA<br />
* xHt - used when drawing elements on the graph (i.e. its xPos)<br />
* yPos - graph position on the vertical axis<br />
* element - the drawn object represented on the graph<br />
* label - graphical Raphael label drawn to left of the y axis<br />
* bBox - the bounding box for event sensing<br />
* longDescription - not used but set to mimic the short description<br />
* shortDescription - used to label the node. (Read from leaf data or created from the
* cluster ID)
*
*
* @class ClusterNode
*/
//Define functions for the methods
/**
* @method isLeaf
* @return {boolean} true if the node is a leaf
*/
function ClusterNode_isLeaf(){
if(this.nType==2){
return true;
}
return false;
}
/**
* @method isRoot
* @return {boolean} true if the node is the root
*/
function ClusterNode_isRoot(){
if(this.nType==0){
return true;
}
return false;
}
/**
* Grows the tree by calling itself recursively.
* First grow down the childA branch. Then grow down the childB branch.
*
* @method grow
* @return {boolean} value not used.
*/
function ClusterNode_grow(nodeIn){
var childIdA=this.cTable.getItemAByIdNo(nodeIn.idNo);
var childIdB=this.cTable.getItemBByIdNo(nodeIn.idNo);
//First grow down the childA branch. Then grow down the childB branch.
//Pass the each CLuster node the table, the id of the new node, the current node
//as the parent, and 4 as unknown type for the new node (i.e. dont know if cluster
//or leaf yet)
this.childA= new ClusterNode(this.cTable,childIdA,nodeIn,4,nodeIn.styles);
//Then grow down the childB branch
this.childB= new ClusterNode(this.cTable,childIdB,nodeIn,4,nodeIn.styles);
return true;//return value not used
}
/**
* Turns on or off the highlighting of a tree branch<br /><br />
* The reason that "this" is not used in this block is due to it being
* involved in event handling. When an event fires "this" becomes the object
* associated with the event rather than the current in scope class object.
* <br /><br />
* Uses a recursive call to highlight itself and all its children.
*
* @method showBranch
*
* @param nodeIn {a ClusterNode} the node at the base of the branch
* @param on {boolean} True indicates turn on highlight. False is off.
* @param isSummaryIn (boolean) True indicates this is a summary dendrogram.
*
*/
function ClusterNode_showBranch(nodeIn,on,isSummaryIn){
//check for nodeIn==null is so return false
if(nodeIn==null){return false;};//attempt no processing on a null node
//check for element null incase element is undrawn in a summary graph
if(nodeIn.element==null){return true;};//attempt no styling if there is no element
//if on = true then show highlighting else revert from highlighting
if(on){
//show highlighting
nodeIn.element.attr(
{
'stroke':'red',
'stroke-width':(nodeIn.styles.strokeWidth*1.5)
});
//if it is a leaf AND NOT s summary chart then highlight the label
if((nodeIn.isLeaf())&&(!isSummaryIn)){
nodeIn.label.attr(
{
'font-style':'italic',
'font-weight':'bold'
});
}
}
else{
//revert from highlighting
nodeIn.element.attr(
{
'stroke':'black',
'stroke-width':nodeIn.styles.strokeWidth
});
//if it is a leaf AND NOT a summary chart then turn of label highlight
if((nodeIn.isLeaf())&&(!isSummaryIn)){
nodeIn.label.attr(
{
'font-weight':'normal',
'font-style':'normal'
});
}
}
//do same for children
nodeIn.showBranch(nodeIn.childA,on,isSummaryIn);
nodeIn.showBranch(nodeIn.childB,on,isSummaryIn);
return true;//has no effect
}
/**
* Draws a transparent rectangular bounding box around the node element
* This will be used as a larger area to be sensitive for interacting with the
* cluster.
*
* @method drawBoundingBox
*
* @param paperIn {Raphael canvas}
*/
function ClusterNode_drawBoundingBox(paperIn){
//make the bounding box
this.bBox=paperIn.rect(
this.element.getBBox().x,
this.element.getBBox().y,
this.element.getBBox().width,
this.element.getBBox().height);
//style it transparent
this.bBox.attr(
{
'fill':'blue',
'stroke-opacity':0,
'fill-opacity':0.1,
'title':this.shortDescription
});
return true;//has no effect
}
/**
* Called by the ClusterNode_getDescendants method.
* It does this by
* calling itself recursively on the node's two children until it comes to a leaf,
* then pushing the leaf onto the array.
*
* @method appendDescendants
*
* @param nodeIn {a ClusterNode} the node for which the list is sought.
* @param arrayIn {array} the array to which any leaves are to be appended (an array of
* ClusterNodes all members of which will be type leaf)
* @return {array of leaf nodes} an array of leaf nodes descended from the given node.
*/
function ClusterNode_appendDescendants(nodeIn,arrayIn){
//check for nodeIn==null if so return arrayIn
if(nodeIn==null){
arrayIn.push("null node");
return arrayIn;
};//attempt no processing on a null node. Should not be needed.
//if the node is a leaf append to array and return
if(nodeIn.nType==2){
//now appending actual node itself
arrayIn.push(nodeIn);
return arrayIn;
}
//arrayIn.push("NodeID="+nodeIn.idNo);//just for tracing
arrayIn=nodeIn.childA.appendDescendants(nodeIn.childA,arrayIn);
arrayIn=nodeIn.childB.appendDescendants(nodeIn.childB,arrayIn);
return arrayIn;
}
/**
* Given a cluster node this returns an array of the leaces descended from
* that cluster node. Calls the appendDescendants method to fill the array.
*
* @method getDescendants
* @param {node} the cluster node for which we want to list the descendant leaves
* @return {array} An array of descendant leaves OR if the node itself is a leaf
* then it will return the node itself.
*/
function ClusterNode_getDescendants(nodeIn){
var resultArray=new Array();
//if this is a leaf then append push node and return
if (nodeIn.nType==2){
resultArray.push(nodeIn);
return resultArray;
}
resultArray=nodeIn.appendDescendants(nodeIn,resultArray);
return resultArray;
}
/**
* Outputs a string listing the descendant leaf nodes of the given node
* Calls the node's getDescendants() method to make an array of descendant leaves
* from which it compiles the string.
*
* @method displayDescendants
* @param nodeIn {a ClusterNode} the node for which the list is sought
* @param guiIn {a Gui} the gui
*
* @return boolean. Value not used.
*/
function ClusterNode_displayDescendants(nodeIn,guiIn){
var resultsArray=nodeIn.getDescendants(nodeIn);//get the descendants
//loop through the array appending them to a string
var resultsString="Descendants of "+nodeIn.shortDescription+": ";
var urlString="?leafList=";
for(var i=0;i<resultsArray.length-1;i++){
resultsString+=resultsArray[i].shortDescription+", ";
urlString+=resultsArray[i].shortDescription+"%2C";
}
resultsString+=resultsArray[resultsArray.length-1].shortDescription;
urlString+=resultsArray[resultsArray.length-1].shortDescription;
guiIn.showMessageWithLink(resultsString,'Node Descendants',urlString );
return true;//has no effect
}
//Constructor function
/**
* If this node is a leaf
* then it sets the child nodes to null and set default leaf descriptions
* and then returns.
* else it calls grow(), causing recursive calls on the new Cluster Node constructor
* to spawn its childA and its childB child nodes.
* (and also sets default cluster descriptions)
*
* @constructor ClusterNode
*
* @param cTableIn {ClusterTable}
* @param idNoIn {number}
* @param parentNodeIn {ClusterNode}
* @param nTypeIn {number}
* @param stylesIn {the Styles object}
*/
function ClusterNode(cTableIn,idNoIn,parentNodeIn,nTypeIn,stylesIn){
//initialise attributes
this.growLimit=2000;
this.cTable=cTableIn;
this.idNo=idNoIn;
this.nType=nTypeIn;
this.styles=stylesIn;
this.parent=parentNodeIn;
this.xHt=cTableIn.getHtByIdNo(idNoIn);
this.rowIndex=cTableIn.getRowIndexByIdNo(idNoIn);
this.leafSproutNo=null;//default for any node. Set later for a leaf
this.currentGroup=1;//default starting group for any node.
this.yPos=null;//graph position on the vertical axis
this.element=null;//the drawn object represented on the graph
this.label=null;//graphical Raphael label drawn to left of the y axis
this.bBox=null;//the bounding box for event sensing
//yPos, element, label, and bBox will be added later after further processing
//associate methods
this.grow = ClusterNode_grow;
this.isLeaf = ClusterNode_isLeaf;
this.isRoot = ClusterNode_isRoot;
this.showBranch=ClusterNode_showBranch;
this.drawBoundingBox=ClusterNode_drawBoundingBox;
this.getDescendants=ClusterNode_getDescendants;
this.appendDescendants=ClusterNode_appendDescendants;
this.displayDescendants=ClusterNode_displayDescendants;
//if this a leaf then set the child nodes to null and set default leaf descriptions
//else grow(), causing a recursive call on the new Cluster Node constructor.
//(also set default cluster descriptions)
if(this.cTable.isLeafByIdNo(this.idNo)){
this.nType=2;
//this.shortDescription="Leaf-"+this.idNo+"-sd";//default. Detail added later
this.shortDescription=cTableIn.leafArray[this.idNo-1].shortDescription;
//this.longDescription=
// "This is the long description of leaf ID "+this.idNo;//as shortDesc
this.longDescription=cTableIn.leafArray[this.idNo-1].longDescription;
this.leafSproutNo=getNextLeafSproutNo();
this.yPos=this.leafSproutNo;
this.childA=null;
this.childB=null;
}else{
this.shortDescription="Cluster ID"+this.idNo;//default. Detail added later
this.longDescription=
"No long description of cluster ID "+this.idNo+" has been set.";//as short
//check grow counter
//if growCounter>some limit don't grow
if (growCounter<this.growLimit){
growCounter++;
this.grow(this);//recursively calls the new ClusterNode
}
}
}
//End of ClusterNode class
//######################################################
//######################################################
/**
* The Canvas class defines the area on which the dendrogram is drawn.
* Holds attributes useful in positioning drawing elements on the dendrogram
* <br /><br />
* It holds some methods key to drawing the elements:<br />
* A drawn element effectively has two kinds of coordinates or position.
* There is its graph position (the logical position on the graph in relation to its
* mergeHt (on the X axis)
* and its yPos along the leaf axis). Then there are its Raphael coordinates determining
* where it is drawn on the canvas, taking into account margins and scaling factors.
* A number of the Canvas methods deal with conversion between the graph (logical)
* coordinates and the Raphael canvas coordinates. (e.g. cvY() takes a logical yPos and
* returns the Raphael canvas y coordinate.)
* <br /><br />
* The constructor sets attributes such as scale factor and margins used in
* drawing dendrogram elements. It uses the window.width property to
* set the width. It creates the Raphael canvas object and
* assigns it to the paper attribute.<br /><br />
*
* Important Canvas attributes used often by other classes: <br />
* margin <br />
* marginLeft<br />
* leafSpace<br />
* leafSpaceSummary<br />
* originHt<br />
* htScaleFactor<br />
* htScaleFactorSummary<br />
* leafLabelSpace<br />
* noOfLeafs<br />
* largestHt<br />
* paper (the Raphael canvas on which all elements are drawn).
*
* @class Canvas
*/
//Methods
/**
* Takes a diagram Y and converts it to a canvas Y,
* taking into account margins and scale factors
*
* @method cvY
* @param yIn {point object} a graph point y pos
* @return {number} the canvas Y
*/
function Canvas_cvY(yIn){
return this.margin+(yIn*this.leafSpace);
}
/**
* Does the same job as cvY but for a summary dendrogram
*
* @method cvYSummary
*
* @param yIn {number} a graph point y pos
* @return {number} the canvas Y
*/
function Canvas_cvYSummary(yIn){
return this.margin+(yIn*this.leafSpaceSummary);
}
/**
* Takes a diagram X and converts it to a canvas X
* taking into account margins and scale factors
*
* @method cvX
* @param xIn {number} a graph point x pos
* @return {number} the canvas X
*/
function Canvas_cvX(xIn){
return this.margin+this.leafLabelSpace+(xIn*this.htScaleFactor);
}
/**
* Takes a diagram X and converts it to a canvas X
* taking into account margins and scale factors
*
* @method cvXSummary
* @param xIn {number} a graph point x pos
* @return {number} the canvas X
*/
function Canvas_cvXSummary(xIn){
return this.margin+this.leafLabelSpace+(xIn*this.htScaleFactorSummary);
}
/**
* Takes a canvas X and converts it to a graph X
* taking into account margins and scale factors
*
* @method graphXfromCvX
*
* @param xIn {number} the canvas point x pos
* @return {number} the converted graph x pos
*/
function Canvas_getGraphXfromCvX(xIn){
return (xIn-this.margin-this.leafLabelSpace)/this.htScaleFactor;
}
/**
* Takes a diagram point and converts it to a canvas point
* taking into account margins and scale factors
*
* @method cVPt
*
* @param ptIn {point object} the diagram point
* @return {point object} the converted point
*/
function Canvas_cVPt(ptIn){
ptIn.x=this.cvX(ptIn.x);
ptIn.y=this.cvY(ptIn.y);
return ptIn;
}
/**
* Takes a Summary diagram point and converts it to a canvas point
* taking into account margins and scale factors
*
* @method cVPtSummary
*
* @param ptIn {point object} the diagram point
* @return {point object} the converted point
*/
function Canvas_cVPtSummary(ptIn){
ptIn.x=this.cvXSummary(ptIn.x);
ptIn.y=this.cvYSummary(ptIn.y);
return ptIn;
}
/**
* Returns the canvas coords (in the form of a point used for the centre of the leaf)
* given a leaf slot number. e.g. if slot is 2 and leafspace is 15 then feed 30 to cvY and return that as y coord
* and feed 0 to cvX and return that as x coord
*
* @method getLeafPt
*
* @param slot {number} the leafe slot number
* @return {point object} a canvas point
*/
function Canvas_getLeafPt(slot){
var y =this.cvY(slot);
return new point(this.cvX(0),y);
}
/**
* Returns the canvas coords (in the form of a point used for the centre of the leaf)
* given a leaf slot number, in the summary graph.<br />
* e.g. if slot is 2 and leafspaceSummary is 4 then feed 8 to cvY and return that as y coord
* and feed 0 to cvX and return that as x coord
*
* @method getLeafPtSummary
*
* @param slot {number} the leaf slot number
* @return {point object} a canvas point
*/
function Canvas_getLeafPtSummary(slot){
var y =this.cvYSummary(slot);
return new point(this.cvX(0),y);
}
/**
* Calculates what the scaleFactor will be for this diagram taking into account
* canvas size and largest merge Ht. This gets assigned to the
* Canvas.htScalefactor attribute
*
* @method calculateScaleFactor
*
* @param largestHt {number} the max in the x axis
* @param widthForCanvas {number} available width
* @param margin {number} to leave round the sides outside the axes
* @param leafLabelSpace {number} space on LH side
*
* @return {number} the scale factor
*/
function Canvas_calculateScaleFactor(largestHt,widthForCanvas,margin,leafLabelSpace){
var result=widthForCanvas-margin-margin-leafLabelSpace;
result=result/(largestHt*1.05);
return result;
}
/**
* Sets new values for xWidth and yHt. Creates a new Raphael canvas in the container
*
* @method createPaper
*
* @param xWidthIn {number} the new width
* @param yHtIn {number} the new ht
*/
function Canvas_createPaper(xWidthIn,yHtIn){
this.xWidth=xWidthIn;
this.yHt=yHtIn;
this.paper = new Raphael(document.getElementById(this.gui.canvasContainer),
xWidthIn, yHtIn);
}
//Constructor function
/**
* @constructor Canvas
*
* @param cTreeIn {the CLusterTree object} the cluster tree to be represented.
* @param cTableIn {the ClusterTable object} the cluster table.
* @param htScaleFactorIn {number} the scale factor in the x direction.
* @param marginIn {number} the size of the margin around the chart
* within the canvas.
* @param leafSpaceIn {number} the amount of space allowed for each leaf on the
* leaf (Y) axis.
* @param leafLabelSpaceIn {number} the amount of extra space down the vertical axis
* to leave for leaf labels.
*/
function Canvas(cTreeIn,cTableIn,marginIn,leafSpaceIn,leafLabelSpaceIn, guiIn){
//initialise attributes
//passed in
this.cTree = cTreeIn;
this.cTable = cTableIn;
this.gui = guiIn;
this.margin = marginIn;
this.leafSpace=leafSpaceIn;
this.leafSpaceSummary=leafSpaceIn/2;
this.originHt=0;//set to higher during summary graph
this.htScaleFactorSummary=null;//set later
this.leafLabelSpace=leafLabelSpaceIn;
this.marginLeft= this.margin+this.leafLabelSpace;
//associate methods
this.cvY=Canvas_cvY;
this.cvYSummary=Canvas_cvYSummary;
this.cvX=Canvas_cvX;
this.cvXSummary=Canvas_cvXSummary;
this.cVPt=Canvas_cVPt;
this.cVPtSummary=Canvas_cVPtSummary;
this.getLeafPt=Canvas_getLeafPt;
this.getLeafPtSummary=Canvas_getLeafPtSummary;
this.calculateScaleFactor=Canvas_calculateScaleFactor;
this.getGraphXfromCvX=Canvas_getGraphXfromCvX;
this.createPaper=Canvas_createPaper;
//derived attributes
this.screenWidth=$(window).width();
$( "#traceDump" ).append("Window.width="+this.screenWidth+", ");
this.widthForCanvas=this.screenWidth-50;// allowing for scroll bars etc
this.noOfLeafs = this.cTable.getNoOfLeaves();
this.largestHt = this.cTree.getRoot().xHt;
this.htScaleFactor=this.calculateScaleFactor(
this.largestHt,this.widthForCanvas,this.margin,this.leafLabelSpace);
$( "#traceDump" ).append("isIPad()="+(navigator.platform == "iPad")+", ");
//declare and draw the canvas
this.createPaper(
(this.largestHt*this.htScaleFactor)+(this.margin*2)+this.leafLabelSpace,
((this.noOfLeafs+1)*this.leafSpace)+(this.margin*2)
);
}
//End of Canvas class
//######################################################
//######################################################
/**
* The Dendrogram class encapulates most of the methods for drawing, removing,
* and redrawing the dendrogram and
* summary dendrogram (Some drawing methods are within the ClusterNode class.)<br /><br />
*
* The constructor draws a dendrogram from the given ClusterTree.<br />
* It calls drawNode() to recursively draw the tree. <br />
* It calls setDragParams() to set up some functions as Raphael drag parameters
* for the threshold bar.<br />
* It calls drawThresholdBar() to draw the threshold bar onto the graph<br />
* It calls this.gui.showThreshold() to display the threshold value<br />
* It calls this.gui.showNoOfGroups(1) to display the no of Groups value initially as 1
* <br /><br />
* Important Dendrogram attributes:<br />
* -thresholdBar - the dragable bar graphic object.<br />
* -groupList - the list of groups formed in response to a user command.<br />
* -isSummary - default false. Set to true if displaying summary dendrogram.<br /><br />
*
* Some attributes are set in the constructor for tuning:<br />
* -leafLabelLength - the number of characters allowed for the leaf labels<br />
* -some thresholdBar dimensions<br />
* -some group band styling attributes<br />
*
* @class Dendrogram class
*
*/
//Methods
/**
* Draws a given cluster node onto the dendrogram.
*
* @method drawCluster
*
* @param nodeIn {ClusterNode object} The cluster node to be drawn
*
* @returns {boolean} but value not used
*/
function Dendrogram_drawCluster(nodeIn){
var parentHt=null;// to hold ht of parent or notional parent ht for root
//if nodeIn is the root set parentHt to nodeIn.xHt*1.05 i.e. 5% higher than max ht
//this will give an area to interact with above the root node
if (nodeIn.isRoot()){
parentHt = nodeIn.xHt*1.05;
}else{
parentHt=nodeIn.parent.xHt;
}
//draw the cluster by starting at the cluster point node.xHt, node.yPos scribing
//over the crossbar section returning to the cluster point and finishing by
//scribing up the stem section creating one shape from a single path call
//create a diagram point for the cluster
var clusterPt=new point(nodeIn.xHt,nodeIn.yPos);
//define the graph points for the cluster with no scaling factors
var clusterPt=new point(nodeIn.xHt,nodeIn.yPos);
var boundA=new point(nodeIn.xHt,nodeIn.childA.yPos);
var boundB=new point(nodeIn.xHt,nodeIn.childB.yPos);
var cEnd=new point(parentHt,nodeIn.yPos);
//now convert them to canvas coordinates incorporating scale factors and margins
clusterPtCv=this.canvas.cVPt(clusterPt);
boundACv=this.canvas.cVPt(boundA);
boundBCv=this.canvas.cVPt(boundB);
cEndCv=this.canvas.cVPt(cEnd);
//now create the required lengths for the path
var cToBoundA= clusterPtCv.y-boundACv.y;
var cToBoundB= clusterPtCv.y-boundBCv.y;
var cToHt= cEnd.x-clusterPtCv.x;
//draw the element as a path on the raphael canvas and assign it to the node element
nodeIn.element=this.canvas.paper.path(
"M "+clusterPtCv.x+" "+clusterPtCv.y+
" l 0 "+cToBoundA+" l 0 "+(-1*cToBoundA)+
" l 0 "+cToBoundB+" l 0 "+(-1*cToBoundB)+
" l "+cToHt+" 0"
);
nodeIn.drawBoundingBox(this.canvas.paper);//draw its bounding box
this.styleCluster(nodeIn);
return true;//has no effect
}
/**
* Draws a given cluster node onto the summary dendrogram.<br /><br />
*
* if the nodeIn is not the root then check parent ht (i.e. ALWAYS draw the root)<br />
* if the xHt of the parent < cutoffIn <br />
* then set element and bounding box to null and return<br />
* else<br />
* set element and bounding box to null<br />
* set the left hand end to be the cutoffIn Ht and draw the new cluster
*
* @method drawClusterSummary
*
* @param cutoffIn {number} the Ht at which the graph is truncated
* @param nodeIn {ClusterNode object} The cluster node to be drawn.
*
* @returns {boolean} but value not used
*/
function Dendrogram_drawClusterSummary(nodeIn,cutoffIn){
if(!nodeIn.isRoot()){
if(nodeIn.parent.xHt<cutoffIn){
nodeIn.element=null;
nodeIn.bBox=null;
return true;
}
}
var parentHt=null;// to hold ht of parent or notional parent ht for root
//if nodeIn is the root set parentHt to nodeIn.xHt*1.05 i.e. 5% higher than max ht
//this will give an area to interact with above the root node
if (nodeIn.isRoot()){
parentHt = nodeIn.xHt*1.05;
}else{
parentHt=nodeIn.parent.xHt;
}
//draw the cluster by starting at the cluster point node.xHt, node.yPos scribing
//over the crossbar section returning to the cluster point and finishing by
//scribing up the stem section creating one shape from a single path call;
//adjust all Hts by subtracting the cutoffIn Ht
//define the graph points for the cluster with no scaling factors
//first check cluster Ht and adust to cutoff if below cutoff Ht
if(nodeIn.xHt<cutoffIn){
var clusterSummaryHt=cutoffIn;
}else{
clusterSummaryHt=nodeIn.xHt;
}
var clusterPt=new point(clusterSummaryHt-cutoffIn,nodeIn.yPos);
var boundA=new point(clusterSummaryHt-cutoffIn,nodeIn.childA.yPos);
var boundB=new point(clusterSummaryHt-cutoffIn,nodeIn.childB.yPos);
var cEnd=new point(parentHt-cutoffIn,nodeIn.yPos);
//now convert them to canvas coordinates incorporating scale factors and margins
clusterPtCv=this.canvas.cVPtSummary(clusterPt);
boundACv=this.canvas.cVPtSummary(boundA);
boundBCv=this.canvas.cVPtSummary(boundB);
cEndCv=this.canvas.cVPtSummary(cEnd);
//now create the required lengths for the path
var cToBoundA= clusterPtCv.y-boundACv.y;
var cToBoundB= clusterPtCv.y-boundBCv.y;
var cToHt= cEnd.x-clusterPtCv.x;
//draw the element as a path on the raphael canvas and assign it to the node element
nodeIn.element=this.canvas.paper.path(
"M "+clusterPtCv.x+" "+clusterPtCv.y+
" l 0 "+cToBoundA+" l 0 "+(-1*cToBoundA)+
" l 0 "+cToBoundB+" l 0 "+(-1*cToBoundB)+
" l "+cToHt+" 0"
);
nodeIn.drawBoundingBox(this.canvas.paper);//draw its bounding box
this.styleCluster(nodeIn);
return true;//has no effect
}
/**
* Styles a given cluster
*
* @method styleCluster
*
* @param nodeIn {ClusterNode object} The cluster node to style (a Node)
*/
function Dendrogram_styleCluster(nodeIn){
//use the attr method to style the axis object
nodeIn.element.attr(
{
'stroke-width':this.strokeWidth,
'stroke-linecap':'round',
title: nodeIn.shortDescription
});
this.addEvents(nodeIn,this.gui,this);//add events to the new node
}
/**
* Styles a given leaf
*
* @method styleLeaf
*
* @param nodeIn {ClusterNode object} The leaf node to style
*/
function Dendrogram_styleLeaf(nodeIn){
//use the attr method to style the axis object
nodeIn.element.attr(
{
'stroke-width':this.strokeWidth,
'stroke-linecap':'round',
//title: "Leaf "+nodeIn.idNo
title: nodeIn.shortDescription
});
this.addEvents(nodeIn,this.gui,this);//add events to the new node
}
/**
* Draws a leaf graphic element
* @method drawLeaf
*
* @param nodeIn {ClusterNode object} The node describing the leaf
*
* @return {boolean} but value not used
*/
function Dendrogram_drawLeaf(nodeIn){
var leafPt=this.canvas.getLeafPt(nodeIn.yPos);//generate a Raphael canvas pixel coordinate
//points and dimension for the stem part
var parentHt=nodeIn.parent.xHt;
var cEnd=new point(parentHt,nodeIn.yPos);
cEndCv=this.canvas.cVPt(cEnd);
var cToHt= cEnd.x-leafPt.x;
//scribe out from the leaf pt to a box around the leaf pt. The box to enclose
//an imaginary circle radius of this.leafRadius.
//Then return to the point on the edge of the box in line with the stem.
//Them scribe the stem up to the parent height (or down to the parent height if the
//parent height is really low or zero.
nodeIn.element=this.canvas.paper.path(
"M "+leafPt.x+" "+leafPt.y+
" l "+this.leafRadius+" 0 "+
"l 0 "+(-1*this.leafRadius)+
" l "+(-2*this.leafRadius)+" 0"+
" l 0 "+(2*this.leafRadius)+
" l "+(2*this.leafRadius)+" 0"+
" l 0 "+(-1*this.leafRadius)+
" l "+(cToHt-this.leafRadius)+" 0"
);
nodeIn.drawBoundingBox(this.canvas.paper);//draw its bounding box
this.styleLeaf(nodeIn);//and add events
return true;//has no effect
}
/**
* Draws a leaf graphic element for the Summary dendrogram. Summary leaves are
* different to those on the normal chart. They have no base and vary in length if
* they are to be drawn at all.<br /><br />
*
* If the xHt of the parent < cutoffIn then set element to null and return<br />
* else<br />
* set the left hand end to be the cutoffIn Ht and draw the leaf<br />
*
* @method drawLeafSummary
*
*
* @param nodeIn {ClusterNode object} The node describing the leaf (a Node)
* @param cutoffIn {number} the Ht at which the graph is truncated
* @returns {boolean} but value not used.
*/
function Dendrogram_drawLeafSummary(nodeIn,cutoffIn){
if(nodeIn.parent.xHt<cutoffIn){
nodeIn.element=null;
nodeIn.bBox=null;
return true;
}
//generate a Raphael canvas pixel coordinate at the truncated Ht
var leafPt=this.canvas.getLeafPtSummary(nodeIn.yPos);
//points and dimension leaf summary line
var parentHt=nodeIn.parent.xHt;
var cEnd=new point(parentHt-cutoffIn,nodeIn.yPos);
var cEndCv=this.canvas.cVPtSummary(cEnd);
var cToHt= cEndCv.x-leafPt.x;
//represent the truncated leaf
//scribe a T shape as for a cluster but the head of the T is 2/3rds the width of a
//leafSpaceSummary. The leg of the T runs from the parent ht down to the cutoff ht,
//i.e. the base line. The T head is needed to give the leaf some body for events
//on its bounding box.
nodeIn.element=this.canvas.paper.path(
"M "+leafPt.x+" "+leafPt.y+
" l 0 "+(-this.canvas.leafSpaceSummary*1/3)+
" l 0 "+(this.canvas.leafSpaceSummary*2/3)+
" l 0 "+(-this.canvas.leafSpaceSummary*1/3)+
" l "+cToHt+" 0"
);
nodeIn.drawBoundingBox(this.canvas.paper);//draw its bounding box
this.styleLeaf(nodeIn);
return true;//has no effect
}
/**
* Draws a text element on the graph next to the leaf.
* @method drawLeafLabel
*
* @param nodeIn {ClusterNode object} The node describing the leaf
*
* @returns {Boolean} but value not used.
*/
function Dendrogram_drawLeafLabel(nodeIn){
//generate a Raphael canvas pixel coordinate eqivalent to the leafPt
var labelPt=this.canvas.getLeafPt(nodeIn.yPos);
//labelPt.x=labelPt.x-this.canvas.leafLabelSpace;//set the x coord back from axis
labelPt.x=labelPt.x-this.leafRadius-4;
nodeIn.label=this.canvas.paper.text(labelPt.x, labelPt.y, nodeIn.shortDescription);
nodeIn.label.attr({'text-anchor': 'end'});
return true;//has no effect
}
/**
* Declares event handlers for the shape using the anonymous function.<br />
* Events for:<br />
* - mouseover, or tap in iPad<br />
* - mouseout, or tap something else in iPad<br />
* - mouse click, or tap in iPad<br /><br />
*
* The parameters are passed here rather than referred to by "this" even
* when they are attributes local to the class because the "this" keyword
* has its scope hijacked by the event. The keyword, "this",
* becomes the event object during an event.
*
* @method addEvents
*
* @param nodeIn {ClusterNode object} The cluster tree node concerned.
* @param guiIn {Gui object} the gui.
* @param dendrogramIn (Dendrogram object) the dendrogram. Needed to avoid "this" keyword
* for event handling
*/
function Dendrogram_addEvents(nodeIn,guiIn,dendrogramIn){
//show branch
//aim is to produce a highlighting of the given cluster and all its childen
nodeIn.bBox.node.onmouseover =
function(){
nodeIn.showBranch(nodeIn, true, dendrogramIn.isSummary);
this.style.cursor = 'pointer';
};
nodeIn.bBox.node.onmouseout =
function(){
nodeIn.showBranch(nodeIn, false, dendrogramIn.isSummary);
this.style.cursor = 'pointer';
};
//display descendants reports details of descendant nodes
nodeIn.bBox.node.onclick =
function(){
nodeIn.displayDescendants(nodeIn,guiIn);
};
}
/**
* Declares event handlers for the shape using the anonymous function.<br />
* Events for:<br />
* - mouseover, or tap in iPad<br />
* - mouseout, or tap something else in iPad<br />
* - mouse click, or tap in iPad<br /><br />
*
* The parameters are passed here rather than referred to by "this" even
* when they are attributes local to the class because the "this" keyword
* has its scope hijacked by the event. The keyword, "this",
* becomes the event object during an event.
*
* @method addEventsToBand
*
* @param groupIn {Group object} group which contains the band for the events
* @param guiIn {Gui object} the gui
* @param dendrogramIn (Dendrogram object) the dendrogram. Needed to avoid "this" keyword
* for event handling
*/
function Dendrogram_addEventsToBand(groupIn,guiIn,dendrogramIn){
//show branch
//aim is to produce a highlighting of the cluster represented
//by the given group and all its childen.
groupIn.band.node.onmouseover =
function(){
groupIn.ancestorNode.showBranch(groupIn.ancestorNode,
true,dendrogramIn.isSummary);
this.style.cursor = 'pointer';
};
groupIn.band.node.onmouseout =
function(){
groupIn.ancestorNode.showBranch(groupIn.ancestorNode,
false, dendrogramIn.isSummary);
this.style.cursor = 'pointer';
};
//display descendants reports details of descendant nodes
groupIn.band.node.onclick =
function(){
groupIn.ancestorNode.displayDescendants(groupIn.ancestorNode,guiIn);
};
}
/**
* Appends cluster groups to a list<br /><br />
* if the nodeIn is above the threshold then call itself recursively on
* childA and child B<br />
* else make a group from its descendants, append it to the list and return
*
* @method appendGroups
* @param nodeIn {Group object} the node being examined.
* @param groupListIn {GroupList object} the groupList to hold the resulting groups.
* @param thresholdIn {number} the threshold value used to define the groups.
*/
function Dendrogram_appendGroups(nodeIn,groupListIn,thresholdIn){
//check for nodeIn==null is so return groupList unchanged
if(nodeIn==null){
return groupListIn;
};//attempt no processing on a null node
//if the node is above the threshold then call itself on childA and child B
if(nodeIn.xHt>thresholdIn){
groupListIn=this.appendGroups(nodeIn.childA,groupListIn,thresholdIn);
groupListIn=this.appendGroups(nodeIn.childB,groupListIn,thresholdIn);
return groupListIn;
}else{
//make a group from this node's descendants and return
//ref Group constructor Group(leafListIn, ancestorNodeIn)
var foundGroup=new Group(nodeIn.getDescendants(nodeIn),nodeIn);
//append to group list
groupListIn.addToEnd(foundGroup);
return groupListIn;
}
}
/**
* Dendrogram_applyGroupingThreshold<br />
* 1) Defines groups of leaves based on a threshold mergeHt.<br />
* 2) Overlays rectangles on the dendrogram to highlight. Those clusters below the
* threshold mergeHt form groups differentiated by colour or shading of the overlayed
* rectangles.<br />
* 3) Display the details of the groups under the Groups tab.<br />
* 4) Feedback to the user via a dialoge that all this has occured.<br /><br />
*
* Given a threshold number, do a depth first search to find the first cluster below
* the threshold (with mergeHt < thresholdIn). The depth first search is carried out
* by the appendGroups() method which calls itself recursively. The search is started
* here by calling appendGroups with the tree root node.
*
* @method applyGroupingThreshold
*
* @param thresholdIn {number} the threshold to be applied.
*/
function Dendrogram_applyGroupingThreshold(thresholdIn){
//This is a depth first search to find the first cluster below the threshold
//Then apply the current group number to all that cluster's decendants
//then advance the group number, ascend to the parent and apply the next group
//number to any remaining group and so on.
//The group number is a GroupList attribute.
//validate the incoming threshold in case it was manually entered
var validThreshold=thresholdIn-0;//Enforces a number
//now check for in being NaN, if so then set it to zero
if (isNaN(validThreshold)){
validThreshold=0;
}
//only allow minimum of originHt
if (validThreshold<this.canvas.originHt){
validThreshold=this.canvas.originHt;
}
//only allow maximum of canvas.largestHt
if (validThreshold>this.canvas.largestHt){
validThreshold=this.canvas.largestHt;
}
var messageString="";//parameter for displayGroupList method
this.groupList.reset();//empty any existing grouplist
this.groupList.threshold=validThreshold;//store the threshold
//first build the group list
//search the tree adding groups to this.groupList
this.groupList=this.appendGroups(this.cTree.getRoot(),this.groupList,validThreshold);
this.displayGroupList(this.groupList,messageString);//output the groups list to the div
this.drawGroupBands(this.groupList);//draw the bandsize());
this.setThresholdBarToMatch(validThreshold);//threshold input may have been keyed
this.gui.showThreshold(validThreshold);//in case an invalid threshold was entered
this.gui.showNoOfGroups(this.groupList.getSize());
}
/**
* Displays a single group in a div on the groups tab
* by appending it to the current contents of the groups tab
*
* @method displayOneGroup
*
* @param leafArrayIn {Array of ClusterNode objects} the leaf array which
* represents the group
* @param groupIdIn {string} a string identifier or label for the group
*/
function Dendrogram_displayOneGroup(leafArrayIn,groupIdIn){
//build the html to put in the group's display div
var outputString=groupIdIn+": <br/>";
//append all but the last item to the string with a comma separator
for(var i=0;i<leafArrayIn.length-1;i++){
outputString+=leafArrayIn[i].shortDescription+", ";
}
//append the last item, no comma
outputString+=leafArrayIn[leafArrayIn.length-1].shortDescription;
//create the group div
var outputHtml='<div class="groupDiv" title="'+groupIdIn +'" id="'+groupIdIn+'">';
outputHtml+=outputString+'</div>';
//append the group div to the current content of the groups tab
var previousContent=$(this.gui.groupsTab).html();
$(this.gui.groupsTab).html(previousContent+outputHtml);
}
/**
* Outputs details of the designated group list.
* Makes calls to this.displayOneGroup() and this.gui.notifyGroupFormation().
*
* @method displayGroupList
*
* @param groupListIn {GroupList object} an array of groups.
* Each group is an array of leaves.
*/
function Dendrogram_displayGroupList(groupListIn,messageIn){
//loop through the list displaying in the div
//var tempLeafArray=new Array();
var resultsSummary=messageIn;
resultsSummary+=groupListIn.getSize()+" groups were formed using";
resultsSummary+=" the threshold set at "+groupListIn.threshold;
resultsSummary+=". See the 'Groups' tab for details.";
//var resultsString="Contents of Group List : ";
var resultsString="";
resultsString+="Threshold: "+groupListIn.threshold+"<br/>";
if(groupListIn.isEmpty()){//if the list is empty say so
resultsString+="The list is empty";
return;
}
//add the introduction to the list into the output div clearing what was there
var groupsIntroHtml='<div class="groupDiv" title="Intro" id="Intro">';
groupsIntroHtml+=resultsString+'</div>';
$(this.gui.groupsTab).html(groupsIntroHtml);
//now trundle through the list displaying the groups
this.displayOneGroup(groupListIn.getFirst().leafList,'Group-1');
var limit=groupListIn.getSize();
for (i=2;i<=limit;i++){
this.displayOneGroup(groupListIn.getNext().leafList,'Group-'+i);
}
//inform user
this.gui.notifyGroupFormation(resultsSummary,groupListIn);
}
/**
* Takes a group and draws an overlay on the graph to delineate it.
* The band is a rectangle of length (the X dimension), thresholdIn.
* Its top and bottom (the Y dimension) defined by groupIn.topBound and
* groupIn.bottomBound<br /><br />
*
* Makes a call on this.addEventsToBand().
*
* @method drawOverlayBand
* @param groupListIn {Group object} an array of leaves.
* @param {number} the threshold defining the groups
*/
function Dendrogram_drawOverlayBand(groupIn,thresholdIn){
//draw the band assigning it to the group's band attribute;
//First gather the coordinates and dimensions
// a raphael rectangle needs x,y,width,and ht.
//Dimensions without scaling or margins:-
//create an origin point for the band rectangle and the top left corner. (At the
//base of the dendrogram)
var bandPt=new point(0,groupIn.topBound-0.5);
// now the rentangle width
//check for summary graph and make allowances
if (this.canvas.originHt>0){
//it is a summary graph
var bandLength=thresholdIn-this.canvas.originHt;
var bandHt=(groupIn.bottomBound-groupIn.topBound)+1;
//now convert them to canvas coordinates incorporating scale factors and margins
bandPt=this.canvas.cVPtSummary(bandPt);
bandLength=bandLength*this.canvas.htScaleFactorSummary;
bandHt=bandHt*this.canvas.leafSpaceSummary;
}else{
//it is a normal graph
var bandLength=thresholdIn;
var bandHt=(groupIn.bottomBound-groupIn.topBound)+1;
//now convert them to canvas coordinates incorporating scale factors and margins
bandPt=this.canvas.cVPt(bandPt);
bandLength=bandLength*this.canvas.htScaleFactor;
bandHt=bandHt*this.canvas.leafSpace;
}
//draw it
groupIn.band=this.canvas.paper.rect(
bandPt.x,
bandPt.y,
bandLength,
bandHt);
//style it transparent
groupIn.band.attr(
{
'fill':this.groupBandColour,
'stroke-opacity':0,
'fill-opacity':this.groupBandOpacity,
'title':'Group from '+groupIn.ancestorNode.shortDescription
});
//add events to the band
this.addEventsToBand(groupIn,this.gui,this);
return true;//has no effect
}
/**
* Loops through the given group list drawing bands on the graph. It
* alternates styling to create banding.<br /><br />
*
* Makes calls on this.drawOverlayBand()
*
* @method drawGroupBands
* @param groupListIn {GroupList object} an array of groups.
*/
function Dendrogram_drawGroupBands(groupListIn){
//loop through the list drawing bands on the graph
if(groupListIn.isEmpty()){//if the list is empty do nothing
return;
}
//now trundle through the list overlaying a band for each.
//draw the overlay for the first band
this.drawOverlayBand(groupListIn.getFirst(),groupListIn.threshold);
//now draw any remaining overlays
var noOfGroups=groupListIn.getSize();
for(var i=2;i<=noOfGroups;i++){
//do the drawing
this.drawOverlayBand(groupListIn.getNext(),groupListIn.threshold);
//and alternate styling
if (i%2==0){
groupListIn.getCurrent().band.attr(
{
'fill':this.groupBandAlternateColour,
'stroke-opacity':0,
'fill-opacity':this.groupBandOpacity
});
}
}
//record that the groups have been drawn
groupListIn.drawn=true;
}
/**
* Draws the tree by starting with the root node and recursively calling itself.
* <br /><br />
*
* Makes calls on this.drawCluster(), this.drawLeaf() and this.drawLeafLabel().
*
* @method drawNode
*
* @param nodeIn {ClusterNode object} the node to be drawn
* @return {boolean} but value not used.
*/
function Dendrogram_drawNode(nodeIn){
//draws the tree by starting with the root node and recursively calling itself
var childA=nodeIn.childA;
var childB=nodeIn.childB;
//if the node is a leaf then draw the leaf and return
//else draw the cluster
if(nodeIn.isLeaf()){
this.drawLeaf(nodeIn);
this.drawLeafLabel(nodeIn);
return true;
}
this.drawCluster(nodeIn);//draw the node
//draw the child nodes
this.drawNode(childA);
this.drawNode(childB);
return true;//has no effect
}
/**
* Draws the summary tree by starting with the root node and recursively calling itself
* <br /><br />
*
* Makes calls on this.drawClusterSummary() and this.drawLeafSummary().
*
* @method drawNodeSummary
*
* @param nodeIn {ClusterNode object} the node to be drawn
* @return {Boolean} but value not used.
*/
function Dendrogram_drawNodeSummary(nodeIn,cutoffIn){
var childA=nodeIn.childA;
var childB=nodeIn.childB;
//if the node is a leaf then draw the leaf and return
//else draw the cluster
if(nodeIn.isLeaf()){
this.drawLeafSummary(nodeIn,cutoffIn);
return true;
}
this.drawClusterSummary(nodeIn,cutoffIn);//draw the node
//draw the child nodes
this.drawNodeSummary(childA,cutoffIn);
this.drawNodeSummary(childB,cutoffIn);
return true;//has no effect
}
/**
* Draws the dragable threshold adjuster bar
*
* @method drawThresholdBar
*
* @return {Boolean} but value not used.
*/
function Dendrogram_drawThresholdBar(){
//set the graph point for the top left of the bar
//It is set a number of leaf spaces above the top so it should protrude up
var barPt=new point(this.canvas.largestHt,this.thresholdBarOverlapTop*-1);
//generate a Raphael canvas pixel coordinate
var barPtCv=this.canvas.cVPt(barPt);
//set the bar length (Ht)
var leafSpace=this.canvas.leafSpace;
if (this.canvas.originHt>0){//it is a summary graph
leafSpace=this.canvas.leafSpaceSummary;
}
var barLength=(this.canvas.noOfLeafs+this.thresholdBarLengthPlus)
*leafSpace;
//draw it
this.thresholdBar=this.canvas.paper.rect(
barPtCv.x,
barPtCv.y,
this.thresholdBarWidth,
barLength);
//style it
this.thresholdBar.attr(
{
'stroke':'green',
'fill':'green',
'stroke-opacity':0.5,
'fill-opacity':0.5,
'title':'Threshold adjuster bar'
});
//make it dragable
this.thresholdBar.drag(this.move,this.dragger,this.up);
return true;//has no effect
}
/**
* Detects the threshold bar position and returns that in terms of the xaxis
* merge Ht.<br /><br />
*
* Makes a call on this.canvas.getGraphXfromCvX().
*
* @method getThresholdPos
*
* @param barIn {graphic object} The threshold bar whose pos is to be detected
*
* @return {number} the xPos of the bar
*/
function Dendrogram_getThresholdPos(barIn){
//var barX=this.thresholdBar.x;//the shape x coord
return this.canvas.getGraphXfromCvX(barIn.x);
}
/**
* Sets Dendrogram properties to serve as parameters for the
* Raphael drag() method.
* Based on Dmitry's Graffle example http://raphaeljs.com/graffle.html<br /><br />
*
* Makes a call on guiIn.showThreshold().
*
* @method setDragParams
*
* @param canvasIn {Canvas object} The canvas
* @param guiIn {Gui object} The gui
*/
function Dendrogram_setDragParams(canvasIn, guiIn){
var originHtNum=canvasIn.originHt-0;//the -0 forces a number type conversion
//##############################################################
// Set up drag method vars for use in drag() method
// -dragger is the storage of the original start pos
// -move is the acceptance of the new coord(s) and shape redraw
// -up is the action to do on release
//###############################################################
this.dragger = function () {
this.ox = this.attr("x");
//this.oy = this.attr("y");//uncomment to unlock y movement
};
this.move = function (dx, dy) {
var att = new Object();
var limit=canvasIn.largestHt;
//limit the movement to with the x axis bounds
if (((this.ox + dx)>=canvasIn.marginLeft)&&((this.ox + dx)<=(canvasIn.cvX(limit)))){
att.x=this.ox + dx;
}else{//limit the move
if((this.ox + dx)<canvasIn.marginLeft){//at low end
att.x=canvasIn.marginLeft;
}else if ((this.ox + dx)>(canvasIn.cvX(limit))){//at high end
att.x=canvasIn.cvX(limit);
}
}
//att.y=this.oy + dy;//uncomment to unlock y movement
this.attr(att);
//thePaper.safari();//forces a redraw
//calculate value to display depending on originHt (affected by summary graph)
if (originHtNum>0){//it is a summary graph
var displayVal=(((att.x-canvasIn.marginLeft)/canvasIn.htScaleFactorSummary));
}else{//just a normal graph
var displayVal= ((att.x-canvasIn.marginLeft)/canvasIn.htScaleFactor);
}
displayVal+=originHtNum;//in the case of summary chart this adds on the cutoff Ht
//format the number
if (displayVal.toPrecision){//if browser supports toPrecision() method
displayVal=displayVal.toPrecision(5);
}
guiIn.showThreshold(displayVal);
};
this.up = function () {
//do this code when the user lets go
//but for now do nothing
};
}
/**
* Sets the threshold bar position in response to some other control
*
* @method setThresholdBarToMatch
*
* @param thresholdIn {number} the threshold to set
* @return {number} the Merge Ht value of the new threshold position
*/
function Dendrogram_setThresholdBarToMatch(thresholdIn){
//Check for this being a summary graph and allow for altered threshold
//position
if (this.canvas.originHt>0){
//it is a summary graph
var endX=((thresholdIn-this.canvas.originHt)*this.canvas.htScaleFactorSummary)
+this.canvas.marginLeft;
}else{
//just a normal graph
var endX=(thresholdIn*this.canvas.htScaleFactor)+this.canvas.marginLeft;
}
var startX=this.thresholdBar.attr('x');
var dX=endX-startX;
this.thresholdBar.translate(dX,0);//translate the bar by the x difference
return thresholdIn;//the merge ht
}
/**
* Sets the threshold bar to the maximum
* merge Ht.
*
* @method setThresholdBarToMax
*
* @return {number} the Merge Ht value of the new threshold position
*/
function Dendrogram_setThresholdBarToMax(){
var startX=this.thresholdBar.attr('x');
//set the end X to be that of the max Ht
var endX=(this.canvas.largestHt*this.canvas.htScaleFactor)+this.canvas.marginLeft;
var dX=endX-startX;
this.thresholdBar.translate(dX,0);
return this.canvas.largestHt;//the merge ht
}
/**
* Sets the threshold bar to zero (or the origin Ht in the case of Summary graph)
*
* @method setThresholdBarToZero
*
*
* @return the Merge Ht value of the new threshold position
*/
function Dendrogram_setThresholdBarToZero(){
var startX=this.thresholdBar.attr('x');
var endX=this.canvas.marginLeft;
var dX=endX-startX;
this.thresholdBar.translate(dX,0);
return this.canvas.originHt;//the merge ht at the orgin
}
/**
* Sets the threshold bar to one step down the
* merge Ht.
*
* @method setThresholdBarToLess
*
* @param fractionIn {number} the fraction of the full travel to make each step
* e.g. 20 means each step is 1/20th of the full travel
*
* @return {number} the Merge Ht value of the new threshold position
*/
function Dendrogram_setThresholdBarToLess(fractionIn){
var startX=this.thresholdBar.attr('x');
//Check for this being a summary graph and allow for altered threshold
//position
if (this.canvas.originHt>0){
//it is a summary graph
var step=((this.canvas.largestHt-this.canvas.originHt)/fractionIn)
*this.canvas.htScaleFactorSummary;
}else{
//just a normal graph
var step=(this.canvas.largestHt/fractionIn)*this.canvas.htScaleFactor;
}
//set the end X to be one step down
var endX=startX-step;
if(endX<this.canvas.marginLeft){//check not off end of chart
endX=this.canvas.marginLeft;
}
var dX=endX-startX;
this.thresholdBar.translate(dX,0);
//Check for this being a summary graph and allow for altered return pos
if (this.canvas.originHt>0){
//it is a summary graph
return ((endX-this.canvas.marginLeft)/this.canvas.htScaleFactorSummary)
+this.canvas.originHt;//the merge ht
}
//just a normal graph
return (endX-this.canvas.marginLeft)/this.canvas.htScaleFactor;//the merge ht
}
/**
* Sets the threshold bar to one step up the
* merge Ht.
*
* @method setThresholdBarToMore
*
* @param fractionIn {number} the fraction of the full travel to make each step
* e.g. 20 means each step is 1/20th of the full travel
*
* @return {number} the Merge Ht value of the new threshold position
*/
function Dendrogram_setThresholdBarToMore(fractionIn){
var startX=this.thresholdBar.attr('x');
//Check for this being a summary graph and allow for altered threshold
//position
if (this.canvas.originHt>0){
//it is a summary graph
var step=((this.canvas.largestHt-this.canvas.originHt)/fractionIn)
*this.canvas.htScaleFactorSummary;
}else{
//just a normal graph
var step=(this.canvas.largestHt/fractionIn)*this.canvas.htScaleFactor;
}
//set the end X to be one step up
var endX=startX+step;
//this next line works for both normal and summary chart ok
if(endX>((this.canvas.largestHt*this.canvas.htScaleFactor)+
this.canvas.marginLeft)){//check not off end of chart
endX=(this.canvas.largestHt*this.canvas.htScaleFactor)+this.canvas.marginLeft;
}
var dX=endX-startX;
this.thresholdBar.translate(dX,0);
//Check for this being a summary graph and allow for altered return pos
if (this.canvas.originHt>0){
//it is a summary graph
return ((endX-this.canvas.marginLeft)/this.canvas.htScaleFactorSummary)
+this.canvas.originHt;//the merge ht
}
//normal graph
return (endX-this.canvas.marginLeft)/this.canvas.htScaleFactor;//the merge ht
}
/**
* Sets the threshold to be one which will give the required no of groups.<br /><br />
*
* Takes the given no of groups and by a binary search sucessively applies a new
* threshold until it finds one that fits the required no of groups and leaves the
* threshold bar set there.<br /><br />
*
* The binary search is limited to 75 iterations if it does not find the right
* spot by then it stops and returns the nearest no of groups it could get.<br /><br />
*
* Group formation can fail to achieve the number of requested groups. There are 4
* ways this can occur<br />
* 1) after 75 binary search iterations if some data set had many many clusters
* at very similar hieghts, it might be that the requested no. of groups was not reachable,
* due to running out of iterations. I recon this is highly unlikely at 75 iterations.<br />
* 2) An impossible number of groups was requested (maybe a decimal, or less than 1, or
* greater than the number of leaves<br />
* 3) if there are any leaves which merge at Ht zero then these cannot be allocated to
* separate groups. They must share a group with their zero Ht cluster mate. <br />
* 4) if creating groups on a summary graph then if the threshold would need to be
* below the cutoff to create the requested number of groups then it won't go below the
* cutoff Ht.<br /><br />
*
* Makes a call on:
* this.displayGroupList()<br />
* this.drawGroupBands()<br />
* this.setThresholdBarToMatch()<br />
* this.gui.showThreshold()<br />
* and<br />
* this.gui.showNoOfGroups()<br /><br />
* Also makes calls on a number of GroupList methods
*
* @method setThresholdForNoOfGroups
*
* @param noOfGroupsIn {number} the required no of groups
*
*/
function Dendrogram_setThresholdForNoOfGroups(noOfGroupsIn){
//eliminate less than 1 and also iron out any erroneous data entered by defaulting
//to 0 if entry is dodgy
var desiredGroups=noOfGroupsIn-0;//Enforces a number
//now check for in being NaN, if so then set it to zero
if (isNaN(desiredGroups)){
desiredGroups=0;
}
//only allow minimum of 1
if (desiredGroups<1){
desiredGroups=1;
}
var currentHigh=this.canvas.largestHt;
var currentLow=this.canvas.originHt;
//var currentGroups=this.canvas.noOfLeafs;
var currentGroups=-1;
var loopCount=0;
var trialThreshold=currentHigh;//set in loop
var message="";//user feedback
//loop while currentGroups<>noIfGroupsIn and loopCount<50
// increment loopCount
// set trialThreshold to hakf way between high and low
// apply trialthreshold.
// set currentThreshold to the generated no of groups
//end loop
//if currentGroups<>noOfGroupsIn then output alternative groups chose diaog
//else indicate sucess
//show the current threshold anyway.
while ( (currentGroups!=desiredGroups)&&(loopCount<75) ){
loopCount++;
if (desiredGroups!=1){//don't try a new threshold of we want just 1 group
trialThreshold=currentLow+(currentHigh-currentLow)/2;
}
if(this.groupList.drawn){//if the grouplist has be drawn to canvas then undraw
this.groupList.reset();
}else{//reset with no undraw
this.groupList.resetButLeaveCanvasAlone();
}
this.groupList.resetButLeaveCanvasAlone();
this.groupList.threshold=trialThreshold;
this.groupList=this.appendGroups(this.cTree.getRoot(),
this.groupList,trialThreshold);
currentGroups=this.groupList.getSize();
if (currentGroups<desiredGroups){//threshold is too high
currentHigh=trialThreshold;
}else{
currentLow=trialThreshold;
}
}
if (currentGroups!=noOfGroupsIn){
//output message about unable to match up. This message is taged on
// the front of the normal dialog about formed groups.
//check for summary chart and output a mesage as appropriate
if(this.canvas.originHt>0){
message= "The desired no. of groups, " + noOfGroupsIn +
" , was not possible. Maybe you did not enter a number or " +
"or you entered a decimal or your number" +
" was outside the number of leaves. Or, as you are working on a " +
"truncated summary " +
"dendrogram at the moment, it is possible that the cutoff is higher " +
"than the threshold would need to be to achieve the required number of " +
"groups. In that case you will " +
"need to restore the full dendrogram and try again. Instead, ";
}else{//this is not a summary chart
message= "The desired no. of groups, " + noOfGroupsIn +
" , was not possible. (The minimum no. of groups that can be formed is " +
"1. There is a theoretical maximum of the number of leaves but if " +
"there are any leaf pairs that cluster at a dissimilarity ht. of zero" +
" then these must share a group). Instead, ";
}
}else{
//indicate total success ... by mentioning nothing extra
message = "";
}
this.displayGroupList(this.groupList,message);//output the groups list to the div
this.drawGroupBands(this.groupList);//draw the bands
this.setThresholdBarToMatch(trialThreshold);//set the threshold bar to match
this.gui.showThreshold(trialThreshold);//display the threshold value
this.gui.showNoOfGroups(currentGroups);//display the final no of groups achieved
return true; // no effect
}
/**
* Removes the groups from the dendrogram <br />
* 1) reset the grouplist<br />
* 2) reset the threshold to max and its field<br />
* 3) reset group no field to 1<br />
* 4) undraw the bands<br /><br />
*
* Makes calls on gui methods and the this.groupList.reset() method.
*
* @method removeGroups
*
*/
function Dendrogram_removeGroups(){
this.groupList.reset();//empty the group list
this.gui.showThreshold(this.setThresholdBarToMax());//reset the threshold
$(this.gui.groupsTab).html("The groups have been removed.");//clear groups tab
this.gui.clearGroupNotifer();//clear the notifyer tab
this.gui.showNoOfGroups(1);//reset the display in the no of Groups field
}
/**
* Removes all of the drawn elements including the axes
*
* @method removeAll
*
*/
function Dendrogram_removeAll(){
//remove any groups
this.removeGroups();
//remove the axes and labels
this.axes.remove();
//remove nodes
//this.removeNode(this.cTree.getRoot());
//remove the threshold bar
this.thresholdBar.remove();
}
/**
* Draws a summary dendrogram truncated at the current threshold ht.<br />
* 1) record the cutoff Ht from the threshold pos<br />
* 2) clear the canvas by removing the paper object<br />
* 3) set a new xaxis scale factor<br />
* 4) create a new paper object of appropriate size<br />
* 5) draw new axes<br />
* 6) draw the summary dendrogram taking into account the cutoff<br />
*
* @method drawSummary
*/
function Dendrogram_drawSummary(){
this.isSummary=true;
var cutoff=this.gui.getThresholdVal()-0;//forces a number
this.setThresholdBarToMatch(cutoff);//threshold input may have been keyed
this.canvas.paper.remove();//erase the paper and all the drawn elements
//remove any groups
this.removeGroups();
//set the summary scale factor in the canvas
this.canvas.htScaleFactorSummary=this.canvas.calculateScaleFactor(
this.canvas.largestHt-cutoff,
this.canvas.widthForCanvas, this.canvas.margin,this.canvas.leafLabelSpace);
//create a new paper for the new chart.
//This requires that the new dimensions be calculated. x (width) is to be the same
//as for original dendrogram
this.canvas.createPaper(this.canvas.xWidth,
((this.canvas.noOfLeafs+1)*this.canvas.leafSpaceSummary)+(this.canvas.margin*2)
);
// draw new axes
this.axes=new GraphAxes(theCanvas,cutoff,false);
this.drawNodeSummary(this.cTree.getRoot(),cutoff);//draw the tree in summary);
//set the canvas origin Ht to cutoff
this.canvas.originHt=cutoff;
this.setDragParams(this.canvas,this.gui);//set up some functions as Raphael drag parameters
this.drawThresholdBar();//draw the threshold tool onto the graph
this.gui.showThreshold(this.canvas.largestHt);//display the threshold value
this.gui.showNoOfGroups(1);//display the no of Groups value
}
/**
* Redraws the dendrogram after displaying truncated graph
* 1) clear the canvas by removing the paper object
* 2) resets the canvas originHt to 0
* 4) create a new paper object of appropriate size
* 5) draw new axes
* 6) draw the dendrogram
*
* @method restore
*/
function Dendrogram_restore(){
this.isSummary=false;
this.canvas.paper.remove();//erase the paper and all the drawn elements
//remove any groups
this.removeGroups();
//set the canvas origin Ht to 0
this.canvas.originHt=0;
//create a new paper for the new chart.
//This requires that the new dimensions be calculated. x (width) is to be the same
//as for original dendrogram
this.canvas.createPaper(this.canvas.xWidth,
((this.canvas.noOfLeafs+1)*this.canvas.leafSpace)+(this.canvas.margin*2)
);
// draw new axes
this.axes=new GraphAxes(theCanvas,0,true);
this.drawNode(this.cTree.getRoot());//draw the tree);
this.setDragParams(this.canvas,this.gui);//set up some functions as Raphael drag parameters
this.drawThresholdBar();//draw the threshold tool onto the graph
this.gui.showThreshold(this.canvas.largestHt);//display the threshold value
this.gui.showNoOfGroups(1);//display the no of Groups value
}
//Constructor function
/**
* Some attributes are set here for tuning
*
* @constructor Dendrogram
*
* @param cTreeIn {ClusterTree object } the filled cluster tree root node
* @param canvasIn {Raphael canvas object} the Raphael canvas on which to draw the chart
* @param stylesIn {Styles object } various styles used in the dendrogram such
* as line thickness.
* @param guiIn {Gui object }an object giving access to user interface methods
* @param axesIn {GraphAxes object} the axes for the graph
*/
function Dendrogram(cTreeIn, canvasIn, stylesIn, guiIn, axesIn){
//initialise attributes
//For Tuning
this.leafLabelLength=12;//number of characters allowed for the leaf labels
this.thresholdBarWidth=20; //number of units wide
this.thresholdBarLengthPlus=9; //number of leafSpaces longer than the chart Ht.
this.thresholdBarOverlapTop=4; //number of leafSpaces it protrudes above the chart.
this.groupBandColour='purple';
this.groupBandAlternateColour='yellow';
this.groupBandOpacity=0.4;
//other attributes
this.isSummary=false;//default. set to true if displaying summary dendrogram
this.styles=stylesIn;
this.cTree = cTreeIn;
this.canvas = canvasIn;
this.leafRadius = stylesIn.leafRadius;
this.strokeWidth = stylesIn.strokeWidth;
this.gui=guiIn;
this.axes=axesIn;
this.thresholdBar=null;// will be the dragable bar but null for now
this.dragger=null;//set later by setDragParams
this.move=null;//set later by setDragParams
this.up=null;//set later by setDragParams
//initialise the Grouplist using the current value of the slider
this.groupList=new GroupList(this.gui.getThresholdVal());
//associate methods
this.drawNode = Dendrogram_drawNode;
this.drawNodeSummary = Dendrogram_drawNodeSummary;
this.drawLeaf = Dendrogram_drawLeaf;
this.drawLeafSummary = Dendrogram_drawLeafSummary;
this.drawLeafLabel = Dendrogram_drawLeafLabel;
this.drawCluster = Dendrogram_drawCluster;
this.drawClusterSummary = Dendrogram_drawClusterSummary;
this.applyGroupingThreshold=Dendrogram_applyGroupingThreshold;
this.appendGroups=Dendrogram_appendGroups;
this.displayGroupList=Dendrogram_displayGroupList;
this.drawGroupBands=Dendrogram_drawGroupBands;
this.drawOverlayBand=Dendrogram_drawOverlayBand;
this.displayOneGroup=Dendrogram_displayOneGroup;
this.drawThresholdBar=Dendrogram_drawThresholdBar;
this.setDragParams=Dendrogram_setDragParams;
this.getThresholdPos=Dendrogram_getThresholdPos;
this.setThresholdBarToMatch=Dendrogram_setThresholdBarToMatch;
this.setThresholdBarToMax=Dendrogram_setThresholdBarToMax;
this.setThresholdBarToZero=Dendrogram_setThresholdBarToZero;
this.setThresholdBarToLess=Dendrogram_setThresholdBarToLess;
this.setThresholdBarToMore=Dendrogram_setThresholdBarToMore;
this.setThresholdForNoOfGroups=Dendrogram_setThresholdForNoOfGroups;
this.removeGroups=Dendrogram_removeGroups;
this.drawSummary=Dendrogram_drawSummary;
this.removeAll=Dendrogram_removeAll;
this.restore=Dendrogram_restore;
this.styleCluster = Dendrogram_styleCluster;
this.styleLeaf = Dendrogram_styleLeaf;
this.addEvents = Dendrogram_addEvents;
this.addEventsToBand = Dendrogram_addEventsToBand;
//processing to be done by the Constructor
this.drawNode(this.cTree.getRoot());//draw the tree
this.setDragParams(this.canvas,this.gui);//set up some functions as Raphael drag parameters
this.drawThresholdBar();//draw the threshold tool onto the graph
this.gui.showThreshold(this.canvas.largestHt);//display the threshold value
this.gui.showNoOfGroups(1);//display the no of Groups value
}
//End of Dendrogram class
//######################################################
//######################################################
//Styles class
/**
* Allows a node object to have its own reference to the styles on hand.
* Makes them readily available when responding to events<br /><br />
*
* This class has no methods
*
* @class Styles
*/
//Define functions for the methods
//none for now
//Constructor function
/**
* @constructor Styles
*
* @param swIn {number} strokeWidth
* @param lrIn {number} leafRadius
* @param cIn {number} nodeHighlightColour
*/
function Styles(swIn,lrIn,cIn){
//initialise attributes
//e.g.
this.strokeWidth=swIn;
this.leafRadius=lrIn;
this.nodeHighlightColour=cIn;
this.animateTime=2000;//used for timing fades etc
//associate methods
//none for now
}
//End of Styles class
//######################################################
//######################################################
//GroupList class
/**
* The group list contains a list of the groups formed from drawing a similarity
* threshold on the dendrogram.
* The list is an array of Groups. The GroupList methods are for interrogating,
* traversing, adding to and removing from the list.
*
* @class GroupList
*/
//Define functions for the methods
/**
* Returns the length of the list
*
* @method getSize
*
* @return {number}
*/
function GroupList_getSize(){
return this.gArray.length;
}
/**
* Returns the first group in the list<br /><br />
* If the list is not empty set pointer to first item and return the first item<br />
* else null
*
* @method getFirst
*
* @return {Group object}
*
*/
function GroupList_getFirst(){
if(!this.isEmpty()){
this.pointer=0;
return this.gArray[0];
}
return null;
}
/**
* Returns the last group in the list<br /><br />
*
* if the list is not empty set pointer to last item and return the last item<br />
* else null
*
* @method getLast
*
* @return {Group object}
*/
function GroupList_getLast(){
if(!this.isEmpty()){
this.pointer=this.gArray.length-1;
return this.gArray[this.gArray.length-1];
}
return null;
}
/**
* Returns the next group in the list<br /><br />
*
* If pointer is less then the list length-1
* then increments pointer and returns the item
* else returns null
*
* @method getNext
*
* @return {Group object}
*/
function GroupList_getNext(){
if (this.pointer<this.gArray.length-1){
this.pointer++;
return this.gArray[this.pointer];
}
return null;
}
/**
* Returns the currently pointed to group in the list<br /><br />
*
* @method getCurrent
*
* @return {Group object}
*/
function GroupList_getCurrent(){
return this.gArray[this.pointer];
}
/**
* Removes a group from the list
*
* @method removeFromEnd
*
* @return {Group object} the popped group
*/
function GroupList_removeFromEnd(){
return this.gArray.pop();
}
/**
* Adds a group to the list
*
* @method addToEnd
*
* @param groupIn {Group object}the group to be added
*/
function GroupList_addToEnd(groupIn){
this.gArray.push(groupIn);
}
/**
* Returns true if ther are no items in the list
*
* @method isEmpty
*
* @return {boolean}
*/
function GroupList_isEmpty(){
if (this.gArray.length==0){
return true;
}
return false;
}
/**
* Resets the group list to be empty and undraws the bands
*
* @method reset
*
* @return {boolean} but value not used.
*/
function GroupList_reset(){
while (!this.isEmpty()){
//empty the array and erase from canvas
this.removeFromEnd().band.remove();
}
this.pointer=-1;
this.drawn=false;
return true;//has no effect
}
/**
* Resets the group list to be empty but leaves the canvas alone.
* Used if no bands have been drawn
*
* @method resetButLeaveCanvasAlone
*
* @return {boolean} but value not used.
*/
function GroupList_resetButLeaveCanvasAlone(){
while (!this.isEmpty()){
//empty the array
this.removeFromEnd();
}
this.pointer=-1;
return true;//has no effect
}
/**
* @constructor GroupList
*/
function GroupList(thresholdIn){
//initialise attributes
this.gArray= new Array();//to hold the Groups
this.pointer=-1;//index of current group
this.threshold=thresholdIn;
this.drawn=false;//records whether or not the group list has been drawn
// onto the graph
//associate methods with defined functions
this.isEmpty=GroupList_isEmpty;
this.addToEnd=GroupList_addToEnd;
this.removeFromEnd=GroupList_removeFromEnd;
this.getCurrent=GroupList_getCurrent;
this.getNext=GroupList_getNext;
this.getSize=GroupList_getSize;
this.getFirst=GroupList_getFirst;
this.getLast=GroupList_getLast;
this.reset=GroupList_reset;
this.resetButLeaveCanvasAlone=GroupList_resetButLeaveCanvasAlone;
}
//End of GroupList class
//######################################################
//######################################################
//Group class
/**
* A group contains a list of leaf nodes (such leaf nodes sharing a common ancestor).
* Groups are collected in a GroupList. <br /><br />
*
* Important attributes:<br />
* A group has attributes topBound and bottomBound which are the yPos of the
* topmost (nearest the origin) and bottommost (furthest from the origin) leaves in
* the group. These are used in drawing the group bands.
*
* @class Group
*/
//Define functions for the methods
/**
* Returns the first item. (A group will never be empty)
*
* @method getFirst
*
* @return {ClusterNode object} a leaf node
*/
function Group_getFirst(){
return this.leafList[0];
}
/**
* Return the last item. (A group will never be empty)
*
* @method getLast
*
* @return {ClusterNode object} a leaf node
*/
function Group_getLast(){
return this.leafList[this.leafList.length-1];
}
//Constructor function
/**
* @constructor Group
*
* @param leafListIn {Array of ClusterNode objects} an array of leaf nodes
* @param ancestorNodeIn {ClusterNode} the node which is the ancestor of all
* the other nodes in the list
*/
function Group(leafListIn, ancestorNodeIn){
//initialise attributes
this.leafList=leafListIn;
this.ancestorNode=ancestorNodeIn;
//this.topBound=this.getFirst().yPos;
this.topBound=leafListIn[0].yPos;
//this.bottomBound=this.getLast().yPos;
this.bottomBound=leafListIn[leafListIn.length-1].yPos;
this.band=null; //a drawn object to be assigned later by the dendrogram
//associate methods
this.getFirst=Group_getFirst;
this.getLast=Group_getLast;
}
//End of Group class
//######################################################
//######################################################
//Gui class
/**
* The Gui class.
* A reference to the instance of this class can be passed to any class that needs
* to interact with the user or output onto the page.<br /><br />
* It has methods for displaying various dialogs and outputing messages. Also I have
* tried to gather in here any references to the DOM page elements such as
* specific divs or buttons. This is so as too isolate the rest of the code from
* these references. Instead the rest of the code accesses the buttons/divs via the
* gui attribute to which they are assigned.<br /><br />
*
* Important Gui attributes:<br />
* -leafSearchURL - the destination for the search link in the groups dialogs<br />
* -mobile - true if a mobile OS is detected. This affects the display behaviour of
* dialogs.<br />
* -ieVersion - holds IE version number or -1 if not IE. (Ended up not using this)<br />
* -isIEvLessThan9 - true if IE AND version less than 9 else false.(Ended up not
* using this)
* <br /><br />
*
* Some tunable attributes affecting dialog display behaviour:<br />
* -maxDialogHt<br />
* -maxMessageLengthToScroll - Beyond this and dialogs are ht limited and scroll.<br />
* <br />
* Attributes referring to names of DOM objects which need accessed by the application:
* <br />
* -groupsTab, thresholdField, groupNoField, canvasContainer, largestMergeHtDiv,
* leafNodesDiv.<br /><br />
*
* The constructor: <br />
* -Calls this.bindTabEvents() to set up the group tab event.<br />
* -Calls the jQuery tabs() method to invoke the tabs effect behaviour on the page<br />
* -Calls the jQuery button() method to invoke the button styling and behaviour on the page
* <br />
*
* @class Gui
*
*/
//Define functions for the methods
/**
* Notifies the user the groups have been formed via a dialog
* Activates the pulsating Groups tab
*
* @method notifyGroupFormation
*
* @param message {string} the message to be shown in the dialog
* @param groupListIn {GroupList object} the group list to be output
*/
function Gui_notifyGroupFormation(message,groupListIn){
//pulsate effect for the groups tab
$('#pulser').addClass('highlight');
$('#pulser').text(groupListIn.getSize()+" Groups");
//Make it pulse 30 times. Each cycle is 1 second
//There is an event set to stop it on tab click
$('#pulser').effect("pulsate", { times:30 }, 1000);
//show the groups dialog
this.showMessage(message,'Groups formed');
}
/**
* Clears groups from the tab notifyer
*
* @method clearGroupNotifer
*
*/
function Gui_clearGroupNotifer(){
//stop it pulsing
$('#pulser').stop(true, true);
//reset the pulser tab text
$('#pulser').text("Groups");
//reset the opacity
$('#pulser').css('opacity', 1);
}
/**
* Show the message dialog using the dialog groups div
* with an OK button
*
* @method showMessage
*
* @param message {string} the message to be shown in the dialog
* @param title {string} the title of the dialog
*/
function Gui_showMessage(message,title){
var dialogWidth=300;//default width
//show the message dialog using the dialog groups div
$( "#dialog-groups" ).html(message);
//check on length of message and adjust width it it is long
if (message.length>this.maxMessageLengthToScroll/5){
dialogWidth=this.maxMessageLengthToScroll/4;
}
//if the message is very long then limit the dialog ht so it scrolls
//but dont do that if it is a mobile device
if ((message.length/2>this.maxMessageLengthToScroll)&& (!this.mobile)){
$( "#dialog-groups" ).dialog({
height: this.maxDialogHt,
width: dialogWidth,
modal: true,
buttons: {
Ok: function() {
$(this).dialog( "close" );
}
}
});
}else{
$( "#dialog-groups" ).dialog({
width: dialogWidth,
modal: true,
buttons: {
Ok: function() {
$(this).dialog( "close" );
}
}
});
}
$("#dialog-groups").dialog('option', 'title', title);
}
/**
* Show the message dialog using the dialog groups div
* with an OK button
* include a hyperlink at the front of the message
*
* @method showMessageWithLink
*
* @param message - the message to be shown in the dialog
* @param title {string} the title of the dialog
* @param urlString {string} the url for the link
*/
function Gui_showMessageWithLink(message,title,urlString){
//assemble the hyperlink html, append the message to it
//and call the show message method
var hyperlinkString='<a href="'+this.leafSearchURL+urlString
+'" target="_blank", >Search Link</a>';
this.showMessage(hyperlinkString+".</br> "+message,title);
}
/**
* Show the value of the threshold adjuster in the gui element for displaying that.
*
* @method showThreshold
*
* @param valIn {number} the value to show
*/
function Gui_showThreshold(valIn){
//format the number
if (valIn.toPrecision){//if browser supports toPrecision() method
valIn=valIn.toPrecision(5);
}
$( this.thresholdField ).val( valIn );
}
/**
* Show the given value in the gui element for displaying no of groups.
*
* @method showNoOfGroups
*
* @param valIn {number} the value to show
*/
function Gui_showNoOfGroups(valIn){
$( this.groupNoField).val( valIn );
}
/**
* Show the given values in the gui element for displaying those.
* used for showning facts about the current loaded data set
*
* @method showDataDisplay
*
* @param h {number} the value to show in largestMergeHtDiv
* @param n {number} the value to show in leafNodesDiv
*/
function Gui_showDataDisplay(h,n){
$( this.largestMergeHtDiv).text( h );
$( this.leafNodesDiv).text( n );
}
/**
* Get the current value of the threshold
*
* @method getThresholdVal
*
* @return {number} the value of the threshold
*/
function Gui_getThresholdVal(){
return $( this.thresholdField).val();
}
/**
* Attach functions to Gui buttons
*
* @method bindButtonFunctions
*
* @param dendrogramIn {Dendrogram object} the dendrogram object with all its functions
* @param guiIn {Gui object } the gui object passed is required in as the "this" keyword
* refernces the event rather diring a click.
*/
function Gui_bindButtonFunctions(dendrogramIn,guiIn){
//bind the click event to the button with its handler
$(function() {
$('#groupingButton').bind('click', function() {
dendrogramIn.applyGroupingThreshold($( "#amount" ).val());
});
$('#removeGroupsButton').bind('click', function() {
dendrogramIn.removeGroups();
});
$('#truncateButton').bind('click', function() {
dendrogramIn.drawSummary();
});
$('#restoreButton').bind('click', function() {
dendrogramIn.restore();
});
$('#maxButton').bind('click', function() {
guiIn.showThreshold(dendrogramIn.setThresholdBarToMax());
});
$('#zeroButton').bind('click', function() {
guiIn.showThreshold(dendrogramIn.setThresholdBarToZero());
});
$('#lessButton').bind('click', function() {
guiIn.showThreshold(dendrogramIn.setThresholdBarToLess(20));
});
$('#moreButton').bind('click', function() {
guiIn.showThreshold(dendrogramIn.setThresholdBarToMore(20));
});
$('#minusButton').bind('click', function() {
guiIn.showThreshold(dendrogramIn.setThresholdBarToLess(190));
});
$('#plusButton').bind('click', function() {
guiIn.showThreshold(dendrogramIn.setThresholdBarToMore(200));
});
$('#useGroupNoButton').bind('click', function() {
dendrogramIn.setThresholdForNoOfGroups($( "#groupNo" ).val());
});
});
}
/**
* Attach functions to Gui tab events
*
* @method bindTabEvents
*
*/
function Gui_bindTabEvents(){
//bind the click event on groups tab to handling code
$(function() {
$('#pulser').bind('click', function() {
//stop it pulsing
$('#pulser').stop(true, true);
//reset the opacity
$('#pulser').css('opacity', 1);
});
});
}
/**
* Attach functions to Gui tab events
*
* @method bindTabEvents
*
*/
function Gui_detectIeVersion()
// Returns the version of Internet Explorer or a -1
// (indicating the use of another browser).
//ref
//http://msdn.microsoft.com/en-us/library/ms537509%28v=vs.85%29.aspx
{
var rv = -1; // Return value assumes failure.
if (navigator.appName == 'Microsoft Internet Explorer')
{
var ua = navigator.userAgent;
var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
if (re.exec(ua) != null)
rv = parseFloat( RegExp.$1 );
}
return rv;
}
//Constructor function
/**
* No parameters. For now this class just provides access to methods
*
* @constructor Gui
*/
function Gui(){
//initialise attributes
this.ieVersion=-1;//default. IE version detected later.
this.isIEvLessThan9=false;//default. IE version detected later.
this.maxDialogHt=$(window).height()*0.75;//dialog is max ht 75% of window
this.maxMessageLengthToScroll=2000;//beyond this and dialogs are ht limited
this.leafSearchURL="http://www.macs.hw.ac.uk/~dar14/project/prototype_1_dendrogr" +
"amer/as_at_Jul06_1500hrs_working/searchStub.php";
//detect mobile device
// Looks for the orientation property.
// In a mobile device such as ipad/android it is defined
if (typeof orientation != 'undefined'){
this.mobile = true;
}else{
this.mobile = false;
}
$( "#traceDump" ).append("gui.mobile="+this.mobile+"</br>");
//names of DOM objects which need accessed by the application
this.groupsTab='#groupsOutput';
this.thresholdField='#amount';
this.groupNoField='#groupNo';
this.canvasContainer='canvas_container';//used in a getElementById call, so no #
this.largestMergeHtDiv='#largestMergeHt';
this.leafNodesDiv='#leafNodes';
//associate methods
this.notifyGroupFormation = Gui_notifyGroupFormation;
this.showMessage = Gui_showMessage;
this.showMessageWithLink = Gui_showMessageWithLink;
this.showThreshold = Gui_showThreshold;
this.bindButtonFunctions = Gui_bindButtonFunctions;
this.bindTabEvents=Gui_bindTabEvents;
this.showNoOfGroups = Gui_showNoOfGroups;
this.clearGroupNotifer = Gui_clearGroupNotifer;
this.getThresholdVal = Gui_getThresholdVal;
this.showDataDisplay = Gui_showDataDisplay;
this.detectIeVersion= Gui_detectIeVersion;
//processing to be done by Constructor
//detect IE version
if((this.ieVersion<9)&&(this.ieVersion>-1)){
this.isIEvLessThan9=true;
}
this.bindTabEvents();//set up the group tab event
//#############################################################################
//jQuery UI elements
//#############################################################################
//#############################################################################
//jQuery tabs Widget
$(function() {
$( "#tabs" ).tabs();
});
//END OF jQuery tabs Widget
//#############################################################################
//#############################################################################
//jQuery buttons
//apply button styling and interaction to the Toolbar buttons.
//function is bound at global level after creation of the dendrogram
$(function() {
$( "#zeroButton" ).button({
text: false,
icons: {
primary: "ui-icon-seek-first"
}
});
$( "#lessButton" ).button({
text: false,
icons: {
primary: "ui-icon-seek-prev"
}
});
$( "#minusButton" ).button({
text: false,
icons: {
primary: "ui-icon-minus"
}
});
$( "#plusButton" ).button({
text: false,
icons: {
primary: "ui-icon-plus"
}
});
$( "#moreButton" ).button({
text: false,
icons: {
primary: "ui-icon-seek-next"
}
});
$( "#maxButton" ).button({
text: false,
icons: {
primary: "ui-icon-seek-end"
}
});
$( "#groupingButton" ).button();
$( "#removeGroupsButton" ).button();
$( "#truncateButton" ).button();
$( "#restoreButton" ).button();
$( "#useGroupNoButton" ).button();
});
//END OF jQuery buttons
//#############################################################################
}
//End of Gui class
//######################################################
});// end of $(document).ready( function() {