Mercurial > hg > webaudioevaluationtool
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