diff analysis/analysis.js @ 654:37f3359709bd

Merge
author Nicholas Jillings <n.g.r.jillings@se14.qmul.ac.uk>
date Thu, 31 Mar 2016 15:48:57 +0100
parents 9f0999472b91
children
line wrap: on
line diff
--- a/analysis/analysis.js	Thu Mar 10 17:07:46 2016 +0000
+++ b/analysis/analysis.js	Thu Mar 31 15:48:57 2016 +0100
@@ -2,11 +2,119 @@
 * Analysis script for WAET
 */
 
-var chartContext;
+// Firefox does not have an XMLDocument.prototype.getElementsByName
+// and there is no searchAll style command, this custom function will
+// search all children recusrively for the name. Used for XSD where all
+// element nodes must have a name and therefore can pull the schema node
+XMLDocument.prototype.getAllElementsByName = function(name)
+{
+    name = String(name);
+    var selected = this.documentElement.getAllElementsByName(name);
+    return selected;
+}
+
+Element.prototype.getAllElementsByName = function(name)
+{
+    name = String(name);
+    var selected = [];
+    var node = this.firstElementChild;
+    while(node != null)
+    {
+        if (node.getAttribute('name') == name)
+        {
+            selected.push(node);
+        }
+        if (node.childElementCount > 0)
+        {
+            selected = selected.concat(node.getAllElementsByName(name));
+        }
+        node = node.nextElementSibling;
+    }
+    return selected;
+}
+
+XMLDocument.prototype.getAllElementsByTagName = function(name)
+{
+    name = String(name);
+    var selected = this.documentElement.getAllElementsByTagName(name);
+    return selected;
+}
+
+Element.prototype.getAllElementsByTagName = function(name)
+{
+    name = String(name);
+    var selected = [];
+    var node = this.firstElementChild;
+    while(node != null)
+    {
+        if (node.nodeName == name)
+        {
+            selected.push(node);
+        }
+        if (node.childElementCount > 0)
+        {
+            selected = selected.concat(node.getAllElementsByTagName(name));
+        }
+        node = node.nextElementSibling;
+    }
+    return selected;
+}
+
+// Firefox does not have an XMLDocument.prototype.getElementsByName
+if (typeof XMLDocument.prototype.getElementsByName != "function") {
+    XMLDocument.prototype.getElementsByName = function(name)
+    {
+        name = String(name);
+        var node = this.documentElement.firstElementChild;
+        var selected = [];
+        while(node != null)
+        {
+            if (node.getAttribute('name') == name)
+            {
+                selected.push(node);
+            }
+            node = node.nextElementSibling;
+        }
+        return selected;
+    }
+}
+
+var chartContext, testData;
 window.onload = function() {
     // Load the Visualization API and the corechart package.
-      google.charts.load('current', {'packages':['corechart']});
+    google.charts.load('current', {'packages':['corechart']});
     chartContext = new Chart();
+    testData = new Data();
+}
+
+function get(url) {
+  // Return a new promise.
+  return new Promise(function(resolve, reject) {
+    // Do the usual XHR stuff
+    var req = new XMLHttpRequest();
+    req.open('GET', url);
+    req.onload = function() {
+      // This is called even on 404 etc
+      // so check the status
+      if (req.status == 200) {
+        // Resolve the promise with the response text
+        resolve(req.response);
+      }
+      else {
+        // Otherwise reject with the status text
+        // which will hopefully be a meaningful error
+        reject(Error(req.statusText));
+      }
+    };
+
+    // Handle network errors
+    req.onerror = function() {
+      reject(Error("Network Error"));
+    };
+
+    // Make the request
+    req.send();
+  });
 }
 
 function arrayMean(values) {
@@ -18,11 +126,19 @@
     return mean;
 }
 
-function percentile(values, n) {
+function percentile(values, p) {
+    //http://web.stanford.edu/class/archive/anthsci/anthsci192/anthsci192.1064/handouts/calculating%20percentiles.pdf
     values.sort( function(a,b) {return a - b;} );
     // get ordinal rank
-    var rank = Math.min(Math.floor(values.length*n/100), values.length-1);
-    return values[rank];
+    var index = values.length*p/100;
+    var k = Math.floor(index);
+    if (k == index) {
+        return values[k];
+    } else {
+        var f = index-k;
+        var x_int = (1-f)*values[k]+f*values[k+1];
+        return x_int;
+    }
 }
 
 function arrayMin(array) {
@@ -47,6 +163,36 @@
     return max;
 }
 
+function boxplotRow(array) {
+    // Take an array of element values and return array of computed intervals
+    var result = {
+        median : percentile(array,50),
+        pct25 : percentile(array,25),
+        pct75 : percentile(array,75),
+        IQR : null,
+        min: null,
+        max: null,
+        outliers: new Array()
+    }
+    result.IQR = result.pct75-result.pct25;
+    var rest = [];
+    var pct75_IQR = result.pct75+1.5*result.IQR;
+    var pct25_IQR = result.pct25-1.5*result.IQR;
+    for (var i=0; i<array.length; i++) {
+        //outliers, ranger above pct75+1.5*IQR or below pct25-1.5*IQR
+        var point = array[i];
+        if (point > pct75_IQR || point < pct25_IQR) {
+            result.outliers.push(point);
+        } else {
+            rest.push(point);
+        }
+    }
+    result.max = arrayMax(rest);
+    result.min = arrayMin(rest);
+    return result;
+    
+}
+
 function arrayHistogram(values,steps,min,max) {
     if (steps == undefined) {
         steps = 0.25;
@@ -77,30 +223,9 @@
 }
 
 function Chart() {
-    this.valueData = null;
-    this.commentData = null;
-    this.loadStatus = 0;
+    this.valueData;
     this.charts = [];
     
-    var XMLHttp = new XMLHttpRequest();
-    XMLHttp.parent = this;
-    XMLHttp.open("GET","../scripts/score_parser.php?format=JSON",true);
-    XMLHttp.onload = function() {
-        // Now we have the JSON data, extract
-        this.parent.valueData = JSON.parse(this.responseText);
-        this.parent.loadStatus++;
-    }
-    XMLHttp.send();
-    var XMLHttp2 = new XMLHttpRequest();
-    XMLHttp2.parent = this;
-    XMLHttp2.open("GET","../scripts/comment_parser.php?format=JSON",true);
-    XMLHttp2.onload = function() {
-        // Now we have the JSON data, extract
-        this.parent.commentData = JSON.parse(this.responseText);
-        this.parent.loadStatus++;
-    }
-    XMLHttp2.send();
-    
     this.chartObject = function(name) {
         // Create the charting object
         this.name = name;
@@ -132,45 +257,10 @@
             this.chart.draw(this.data,this.options);
         }
         this.sortData = function() {
-            
-            var map = this.data.Jf.map(function(el,i){
-                return {index: i, value: el.c[1].v};
-            });
-            
-            map.sort(function(a,b){
-                if (a.value > b.value) {return -1;}
-                if (a.value < b.value) {return 1;}
-                return 0;
-            })
-            
-            var Jf = [];
-            var cc = [];
-            for (var i=0; i<map.length; i++) {
-                Jf.push(this.data.Jf[map[i].index]);
-                cc.push(this.data.cc[map[i].index]);
-            }
-            this.data.Jf = Jf;
-            this.data.cc = cc;
+            this.data.sort(1);
         }
         this.sortName = function() {
-            var map = this.data.Jf.map(function(el,i){
-                return {index: i, value: el.c[0].v};
-            });
-            
-            map.sort(function(a,b){
-                if (a.value < b.value) {return -1;}
-                if (a.value > b.value) {return 1;}
-                return 0;
-            })
-            
-            var Jf = [];
-            var cc = [];
-            for (var i=0; i<map.length; i++) {
-                Jf.push(this.data.Jf[map[i].index]);
-                cc.push(this.data.cc[map[i].index]);
-            }
-            this.data.Jf = Jf;
-            this.data.cc = cc;
+            this.data.sort(0);
         }
         this.handleEvent = function() {
             // Only used to handle the chart.event.addListener(this,'ready') callback
@@ -203,21 +293,35 @@
         this.buildTable = function() {
             var table = document.createElement("table");
             table.border = "1";
-            for (var rowIndex=0; rowIndex<this.data.If.length; rowIndex++) {
-                var row = document.createElement("tr");
-                table.appendChild(row);
-                var rowTitle = document.createElement("td");
-                rowTitle.textContent = this.data.If[rowIndex].label;
-                row.appendChild(rowTitle);
-                for (var cIndex=0; cIndex<this.data.cc.length; cIndex++) {
-                    var column = document.createElement("td");
-                    column.textContent = this.data.cc[cIndex][rowIndex].tf;
-                    row.appendChild(column);
+            var numRows = this.data.getNumberOfRows();
+            var numColumns = this.data.getNumberOfColumns();
+            for (var columnIndex=0; columnIndex<numColumns; columnIndex++)
+            {
+                var tableTitle = this.data.getColumnLabel(columnIndex);
+                if (tableTitle != "") {
+                    var table_row = document.createElement('tr');
+                    table.appendChild(table_row);
+                    var row_title = document.createElement('td');
+                    table_row.appendChild(row_title);
+                    row_title.textContent = tableTitle;
+                    for (var rowIndex=0; rowIndex<numRows; rowIndex++)
+                    {
+                        var row_entry = document.createElement('td');
+                        table_row.appendChild(row_entry);
+                        var entry = this.data.getValue(rowIndex,columnIndex);
+                        if (isFinite(Number(entry)))
+                        {
+                            entry = String(Number(entry).toFixed(4));
+                        }
+                        row_entry.textContent = entry;
+                    }
                 }
             }
             this.tableDOM.appendChild(table);
         };
         this.writeLatex = function() {
+            var numRows = this.data.getNumberOfRows();
+            var numColumns = this.data.getNumberOfColumns();
             var root = document.createElement("div");
             root.className = "code";
             var holder = document.createElement("pre");
@@ -225,21 +329,32 @@
             var start = document.createElement("p");
             start.textContent = "\\" + "begin{tabular}{|l|";
             holder.appendChild(start);
-            for (var i=0; i<this.data.cc.length; i++) {
+            for (var i=0; i<numRows; i++) {
                 start.textContent = start.textContent+"c|";
             }
             start.textContent = start.textContent.concat("}");
             // Now write the rows:
-            for (var rIndex=0; rIndex<this.data.If.length; rIndex++) {
-                var row = document.createElement("p");
-                row.textContent = this.data.If[rIndex].label.concat(" & ");
-                for (var cIndex=0; cIndex<this.data.cc.length; cIndex++) {
-                    row.textContent = row.textContent.concat(this.data.cc[cIndex][rIndex].tf);
-                    if (cIndex < this.data.cc.length-1) {
-                        row.textContent = row.textContent.concat(" & ");
+            for (var rIndex=0; rIndex<numColumns; rIndex++) {
+                var tableTitle = this.data.getColumnLabel(rIndex);
+                if(tableTitle != "")
+                {
+                    var row = document.createElement("p");
+                    row.textContent = tableTitle.concat(" & ");
+                    for (var cIndex=0; cIndex<numRows; cIndex++) {
+                        var entry = this.data.getValue(cIndex,rIndex);
+                        if (isFinite(Number(entry)))
+                        {
+                            entry = String(Number(entry).toFixed(4));
+                        }
+                        row.textContent = row.textContent.concat(entry);
+                        if (cIndex < numRows-1) {
+                            row.textContent = row.textContent.concat(" & ");
+                        } else {
+                            row.textContent = row.textContent.concat(" \\\\ \\hline");
+                        }
                     }
+                    holder.appendChild(row);
                 }
-                holder.appendChild(row);
             }
             // Table end
             var end = document.createElement("p");
@@ -273,9 +388,9 @@
                     // Find the axis
                     var axisChart = chartList.find(function(element,index,array){
                         if (element.name == this) {return true;} else {return false;}
-                    },"mean-test-"+axis.id);
+                    },"mean-test-"+axis.name);
                     if (axisChart == null) {
-                        axisChart = new this.chartObject("mean-test-"+axis.id);
+                        axisChart = new this.chartObject("mean-test-"+axis.name);
                         axisChart.options = {
                             'title':'Mean of axis: '+axis.name,
                             'width':window.innerWidth*0.9,
@@ -302,6 +417,77 @@
         }
     }
     
+    this.drawTestBoxplot = function() {
+        if (this.valueData == null) {
+            console.log("Error - Data not loaded");
+            return;
+        }
+        var chartList = [];
+        
+        // Creates one chart per axis
+        
+        // Create the data table
+        for (var page of this.valueData.pages) {
+            for (var element of page.elements) {
+                for (var axis of element.axis) {
+                    // Find the axis
+                    var axisChart = chartList.find(function(element,index,array){
+                        if (element.name == this) {return true;} else {return false;}
+                    },"boxplot-test-"+axis.name);
+                    if (axisChart == null) {
+                        // Axis chart doesn't exist
+                        axisChart = new this.chartObject("boxplot-test-"+axis.name);
+                        axisChart.options = {
+                            'title':'Boxplot of axis '+axis.name,
+                            'width':window.innerWidth*0.9,
+                            'height':(window.innerWidth*0.9)/1.77,
+                            legend: {position: 'none'},
+                            lineWidth: 0,
+                            series: [{'color': '#D3362D'}],
+                            intervals: {
+                                barWidth: 1,
+                                boxWidth: 1,
+                                lineWidth: 2,
+                                style: 'boxes'
+                            },
+                            interval: {
+                                max: {
+                                    style: 'bars',
+                                    fillOpacity: 1,
+                                    color: '#777'
+                                },
+                                min: {
+                                    style: 'bars',
+                                    fillOpacity: 1,
+                                    color: '#777'
+                                }
+                            }
+                        };
+                        axisChart.data.addColumn('string','id');
+                        axisChart.data.addColumn('number','median');
+                        axisChart.data.addColumn({id:'max',type:'number',role:'interval'});
+                        axisChart.data.addColumn({id:'min',type:'number',role:'interval'});
+                        axisChart.data.addColumn({id:'firstQuartile',type:'number',role:'interval'});
+                        axisChart.data.addColumn({id:'median',type:'number',role:'interval'});
+                        axisChart.data.addColumn({id:'thirdQuartile',type:'number',role:'interval'});
+                        chartList.push(axisChart);
+                        document.getElementById("test-pages").appendChild(axisChart.root);
+                    }
+                    var result = boxplotRow(axis.values);
+                    axisChart.data.addRow([element.id,result.median,result.max,result.min,result.pct25,result.median,result.pct75]);
+                }
+            }
+        }
+        // Build and push charts
+        for (var chart of chartList) {
+            chart.chart = new google.visualization.LineChart(chart.chartDOM);
+            chart.chart.draw(chart.data,chart.options);
+            chart.buildTable();
+            chart.writeLatex();
+            this.charts.push(chart);
+        }
+    }
+    
     this.drawPageMean = function() {
         // First we must get the value data
         if (this.valueData == null) {
@@ -392,4 +578,253 @@
             }
         }
     }
+}
+
+function Data() {
+    // This holds the link between the server side calculations and the client side visualisation of the data
+    
+    // Dynamically generate the test filtering / page filterting tools
+    var self = this;
+    // Collect the test types and counts
+    this.testSavedDiv = document.getElementById("test-saved");
+    this.testSaves = null;
+    this.selectURL = null;
+    
+    this.specification = new Specification();
+    get("../test-schema.xsd").then(function(response){
+        var parse = new DOMParser();
+        self.specification.schema = parse.parseFromString(response,'text/xml');
+    },function(error){
+        console.log("ERROR: Could not get Test Schema");
+    });
+    this.update = function(url) {
+        var self = this;
+    }
+    
+    this.updateData = function(req_str) {
+        // Now go get that data
+        get(req_str).then(function(response){
+            // Returns the data
+            chartContext.valueData = JSON.parse(response);
+        },function(error){console.error(error);});
+    }
+}
+
+var interfaceContext = new function() {
+    // This creates the interface for the user to connect with the dynamic back-end to retrieve data
+    this.rootDOM = document.createElement("div");
+    this.getDataButton = {
+        button: document.createElement("button"),
+        parent: this,
+        handleEvent: function(event) {
+            // Get the list of files:
+            var req_str = "../scripts/get_filtered_score.php"+this.parent.getFilterString();
+            testData.updateData(req_str);
+        }
+    }
+    this.getDataButton.button.textContent = "Get Filtered Data";
+    this.getDataButton.button.addEventListener("click",this.getDataButton);
+    
+    this.testSaves = {
+        json: null,
+        selectedURL: null,
+        inputs: [],
+        parent: this
+    };
+    this.init = function() {
+        var self = this;
+        get('../scripts/get_tests.php?format=JSON').then(function(response){
+            document.getElementById("test-saved").innerHTML = null;
+            var table = document.createElement("table");
+            table.innerHTML = "<tr><td>Test Filename</td><td>Count</td><td>Include</td></tr>";
+            self.testSaves.json = JSON.parse(response);
+            for (var test of self.testSaves.json.tests) {
+                var tableRow = document.createElement("tr");
+                var tableRowFilename = document.createElement("td");
+                tableRowFilename.textContent = test.testName;
+                var tableRowCount = document.createElement("td");
+                tableRowCount.textContent = test.files.length;
+                tableRow.appendChild(tableRowFilename);
+                tableRow.appendChild(tableRowCount);
+                var tableRowInclude = document.createElement("td");
+                var obj = {
+                    root: document.createElement("input"),
+                    parent: self.testSaves,
+                    handleEvent: function(event) {
+                        this.parent.selectedURL = event.currentTarget.getAttribute("source");
+                        var self = this;
+                        get(this.parent.selectedURL).then(function(response){
+                            var parse = new DOMParser();
+                            testData.specification.decode(parse.parseFromString(response,'text/xml'));
+                            self.parent.parent.generateFilters(testData.specification);
+                            self.parent.parent.getFileCount();
+                            return true;
+                        },function(error){
+                            console.log("ERROR: Could not get"+url);
+                            return false;
+                        });
+                    }
+                }
+                obj.root.type = "radio";
+                obj.root.name = "test-include";
+                obj.root.setAttribute("source",test.testName);
+                obj.root.addEventListener("change",obj);
+                tableRowInclude.appendChild(obj.root);
+                tableRow.appendChild(tableRowInclude);
+                table.appendChild(tableRow);
+                self.testSaves.inputs.push(obj);
+            }
+            document.getElementById("test-saved").appendChild(table);
+        },function(error){console.error(error);});
+    }
+    
+    this.filterDOM = document.createElement("div");
+    this.filterDOM.innerHTML = "<p>PreTest Filters</p><div id='filter-count'></div>";
+    this.filterObjects = [];
+    this.generateFilters = function(specification) {
+        // Filters are based on the pre and post global surverys
+        var FilterObject = function(parent,specification) {
+            this.parent = parent;
+            this.specification = specification;
+            this.rootDOM = document.createElement("div");
+            this.rootDOM.innerHTML = "<span>ID: "+specification.id+", Type: "+specification.type+"</span>";
+            this.rootDOM.className = "filter-entry";
+            this.handleEvent = function(event) {
+                switch(this.specification.type) {
+                    case "number":
+                        var name = event.currentTarget.name;
+                        eval("this."+name+" = event.currentTarget.value");
+                        break;
+                    case "checkbox":
+                        break;
+                    case "radio":
+                        break;
+                }
+                this.parent.getFileCount();
+            }
+            this.getFilterPairs = function() {
+                var pairs = [];
+                switch(this.specification.type) {
+                    case "number":
+                        if (this.min != "") {
+                            pairs.push([specification.id+"-min",this.min]);
+                        }
+                        if (this.max != "") {
+                            pairs.push([specification.id+"-max",this.max]);
+                        }
+                        break;
+                    case "radio":
+                    case "checkbox":
+                        for (var i=0; i<this.options.length; i++) {
+                            if (!this.options[i].checked) {
+                                pairs.push([specification.id+"-exclude-"+i,specification.options[i].name]);
+                            }
+                        }
+                        break;
+                }
+                return pairs;
+            }
+            switch(specification.type) {
+                case "number":
+                    // Number can be ranged by min/max levels
+                    this.min = "";
+                    this.max = "";
+                    this.minDOM = document.createElement("input");
+                    this.minDOM.type = "number";
+                    this.minDOM.name = "min";
+                    this.minDOM.addEventListener("change",this);
+                    this.minDOMText = document.createElement("span");
+                    this.minDOMText.textContent = "Minimum: ";
+                    var pairHolder = document.createElement("div");
+                    pairHolder.appendChild(this.minDOMText);
+                    pairHolder.appendChild(this.minDOM);
+                    this.rootDOM.appendChild(pairHolder);
+                    
+                    this.maxDOM = document.createElement("input");
+                    this.maxDOM.type = "number";
+                    this.maxDOM.name = "max";
+                    this.maxDOM.addEventListener("change",this);
+                    this.maxDOMText = document.createElement("span");
+                    this.maxDOMText.textContent = "Maximum: ";
+                    var pairHolder = document.createElement("div");
+                    pairHolder.appendChild(this.maxDOMText);
+                    pairHolder.appendChild(this.maxDOM);
+                    this.rootDOM.appendChild(pairHolder);
+                    break;
+                case "radio":
+                case "checkbox":
+                    this.options = [];
+                    for (var i=0; i<specification.options.length; i++) {
+                        var option = specification.options[i];
+                        var pairHolder = document.createElement("div");
+                        var text = document.createElement("span");
+                        text.textContent = option.text;
+                        var check = document.createElement("input");
+                        check.type = "checkbox";
+                        check.setAttribute("option-index",i);
+                        check.checked = true;
+                        check.addEventListener("click",this);
+                        this.options.push(check);
+                        pairHolder.appendChild(text);
+                        pairHolder.appendChild(check);
+                        this.rootDOM.appendChild(pairHolder);
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+        for (var survey_entry of specification.preTest.options.concat(specification.postTest.options)) {
+            switch(survey_entry.type) {
+                case "number":
+                case "radio":
+                case "checkbox":
+                    var node = new FilterObject(this,survey_entry);
+                    this.filterObjects.push(node);
+                    this.filterDOM.appendChild(node.rootDOM);
+                    break;
+                default:
+                    break;
+            }
+        }
+        document.getElementById("test-saved").appendChild(this.filterDOM);
+        document.getElementById("test-saved").appendChild(this.getDataButton.button);
+    }
+    this.getFilterString = function() {
+        var pairs = [];
+        for (var obj of this.filterObjects) {
+            pairs = pairs.concat(obj.getFilterPairs());
+        }
+        var req_str = "?url="+this.testSaves.selectedURL;
+        var index = 0;
+        while(pairs[index] != undefined) {
+            req_str += '&';
+            req_str += pairs[index][0]+"="+pairs[index][1];
+            index++;
+        }
+        return req_str;
+    }
+    this.getFilteredUrlArray = function() {
+        var req_str = "../scripts/get_filtered_count.php"+this.getFilterString();
+        return get(req_str).then(function(response){
+            var urls = JSON.parse(response);
+            return urls.urls;
+        },function(error){
+            console.error(error);
+        });
+    }
+    this.getFileCount = function() {
+        // First we must get the filter pairs
+        this.getFilteredUrlArray().then(function(response){
+            var str = "Filtered to "+response.length+" file";
+            if (response.length != 1) {
+                str += "s.";
+            } else {
+                str += ".";
+            }
+            document.getElementById("filter-count").textContent = str;
+        },function(error){});
+    }
+    
+    this.init();
 }
\ No newline at end of file