view analysis/analysis.js @ 2376:c41caaa96633

Some fixes for #90. Also a failsafe loop if the server never responds with meaningul information from saves (for instance, running only on apache or basic http servers). More changes to pythonServer for python 3.5. Please check if still valid on 2.7
author Nicholas Jillings <nicholas.jillings@mail.bcu.ac.uk>
date Thu, 19 May 2016 10:44:19 +0100
parents 0141cbd1169b
children 464c6c6692d6
line wrap: on
line source
/*
* Analysis script for WAET
*/

// 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']});
    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) {
    var mean = 0;
    for (var value of values) {
        mean += value;
    }
    mean /= values.length;
    return mean;
}

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 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) {
    // Return the minimum value of an array
    var min = array[0];
    for (var value of array) {
        if (value < min) {
            min = value;
        }
    }
    return min;
}

function arrayMax(array) {
    // Return the minimum value of an array
    var max = array[0];
    for (var value of array) {
        if (value > max) {
            max = value;
        }
    }
    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;
        console.log("Warning: arrayHistogram called without steps size set, default to 0.25");
    }
    if (min == undefined) {min = arrayMin(values);}
    if (max == undefined) {max = arrayMax(values);}
    var histogram = [];
    var index = min;
    while(index < max) {
        histogram.push({
            marker: index,
            lt: index,
            rt: index+steps,
            count: 0
        });
        index += steps;
    }
    for (var value of values) {
        for (var entry of histogram) {
            if (value >= entry.lt && value <= entry.rt) {
                entry.count++;
                break;
            }
        }
    }
    return histogram;
}

function Chart() {
    this.valueData;
    this.charts = [];
    
    this.chartObject = function(name) {
        // Create the charting object
        this.name = name;
        this.root = document.createElement("div");
        this.root.className = "chart-holder";
        this.root.setAttribute("name",name);
        this.chartDOM = document.createElement("div");
        this.tableDOM = document.createElement("div");
        this.latexDOM = document.createElement("div");
        this.downloadDOM = document.createElement("div");
        this.chart = undefined;
        this.data = new google.visualization.DataTable();
        this.options = {};
        this.print = document.createElement("button");
        this.sortDataButton = document.createElement("button");
        this.sortDataButton.textContent = "Sort by Data";
        this.sortDataButton.addEventListener("click",this);
        this.sortDataButton.setAttribute("name","sort-data");
        this.sortNameButton = document.createElement("button");
        this.sortNameButton.textContent = "Sort by Name";
        this.sortNameButton.addEventListener("click",this);
        this.sortNameButton.setAttribute("name","sort-name");
        this.draw = function() {
            if (this.chart == undefined) {return;}
            this.tableDOM.innerHTML = null;
            this.latexDOM.innerHTML = null;
            this.buildTable();
            this.writeLatex();
            this.chart.draw(this.data,this.options);
        }
        this.sortData = function() {
            this.data.sort(1);
        }
        this.sortName = function() {
            this.data.sort(0);
        }
        this.handleEvent = function() {
            // Only used to handle the chart.event.addListener(this,'ready') callback
            switch(event.currentTarget.getAttribute("name"))
            {
                case "download":
                    window.open(this.chart.getImageURI());
                    break;
                case "sort-data":
                    this.sortData();
                    this.draw();
                    break;
                case "sort-name":
                    this.sortName();
                    this.draw();
                    break;
            }
        }
        
        this.root.appendChild(this.chartDOM);
        this.root.appendChild(this.tableDOM);
        this.root.appendChild(this.latexDOM);
        this.root.appendChild(this.sortDataButton);
        this.root.appendChild(this.sortNameButton);
        this.root.appendChild(this.print);
        this.print.textContent = "Download";
        this.print.setAttribute("name","download");
        this.print.addEventListener("click",this);
        this.root.appendChild(this.downloadDOM);
        this.buildTable = function() {
            var table = document.createElement("table");
            table.border = "1";
            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");
            // Table start
            var start = document.createElement("p");
            start.textContent = "\\" + "begin{tabular}{|l|";
            holder.appendChild(start);
            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<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);
                }
            }
            // Table end
            var end = document.createElement("p");
            end.textContent = "\\" + "end{tabular}";
            holder.appendChild(end);
            root.appendChild(holder);
            this.latexDOM.appendChild(root);
        }
    }
    
    this.clear = function() {
        var inject = document.getElementById("test-pages");
        for (var chart of this.charts) {
            inject.removeChild(chart.root);
        }
        this.charts = [];
    }
    
    this.drawTestMean = function() {
        // This draws one bargraph per axis with every test element on
        if (this.valueData == null) {
            console.log("Error - Data not loaded");
            return;
        }
        var chartList = [];
        
        // 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;}
                    },"mean-test-"+axis.name);
                    if (axisChart == null) {
                        axisChart = new this.chartObject("mean-test-"+axis.name);
                        axisChart.options = {
                            'title':'Mean of axis: '+axis.name,
                            'width':window.innerWidth*0.9,
                            'height':(window.innerWidth*0.9)/1.77
                        }
                        axisChart.data.addColumn('string','id');
                        axisChart.data.addColumn('number',axis.name);
                        chartList.push(axisChart);
                        document.getElementById("test-pages").appendChild(axisChart.root);
                    }
                    var mean = arrayMean(axis.values);
                    axisChart.data.addRow([element.id,mean]);
                }
            }
        }
        
        // Build and push charts
        for (var chart of chartList) {
            chart.chart = new google.visualization.ColumnChart(chart.chartDOM);
            chart.chart.draw(chart.data,chart.options);
            chart.buildTable();
            chart.writeLatex();
            this.charts.push(chart);
        }
    }
    
    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) {
            console.log("Error - Data not loaded");
            return;
        }
        // We create one plot per page
        for (var page of this.valueData.pages) {
            
            // Create the chart resulting point
            var chart = new this.chartObject("mean-page-"+page.id);
            document.getElementById("test-pages").appendChild(chart.root);
            
            // Create the data table
            chart.data.addColumn('string','id');
            // Get axis labels
            for (var axis of page.elements[0].axis) {
                chart.data.addColumn('number',axis.name);
            }
            var rows = []; // Rows is an array of tuples [col1, col2, col3 ... colN];
            for (var element of page.elements) {
                var entry = [element.id];
                for (var i=0; i<page.elements[0].axis.length; i++) {
                    var mean =0;
                    if (i < element.axis.length) {
                        var axis = element.axis[i];
                        mean = arrayMean(axis.values);
                    }
                    entry.push(mean);
                }
                rows.push(entry);
            }
            chart.data.addRows(rows);
            chart.options = {
                'title':'Mean of page: '+page.id,
                'width':800,
                'height':700
            }
            // Draw the chart
            chart.chart = new google.visualization.ColumnChart(chart.chartDOM);
            chart.chart.draw(chart.data,chart.options);
            chart.buildTable();
            chart.writeLatex();
            this.charts.push(chart);
        }
    }
    
    this.drawElementHistogram = function() {
        // First we must get the value data
        if (this.valueData == null) {
            console.log("Error - Data not loaded");
            return;
        }
        // We create one plot per element, enjoy...
        for (var page of this.valueData.pages) {
            for (var element of page.elements) {
                // Build the chart object
                var chart = new this.chartObject("histogram-element-"+element.id);
                document.getElementById("test-pages").appendChild(chart.root);
                chart.data.addColumn('string','index');
                var histograms = [];
                for (var axis of element.axis) {
                    chart.data.addColumn('number',axis.name);
                    histograms.push(arrayHistogram(axis.values,0.125,0.0,1.0));
                }
                for (var axis of element.axis) {
                    for (var i=0; i<histograms[0].length; i++)
                    {
                        var entry = [""+histograms[0][i].lt.toPrecision(2)+"-"+histograms[0][i].rt.toPrecision(3)]
                        for (var histogram of histograms) {
                            entry.push(histogram[i].count);
                        }
                        chart.data.addRow(entry);
                    }
                }
                chart.options = {
                    'title':'Histogram of element: '+element.id,
                    'width':800,
                    'height':700,
                    'bar':{'groupWidth': '100%'}
                }
                // Draw the chart
                chart.chart = new google.visualization.ColumnChart(chart.chartDOM);
                chart.chart.draw(chart.data,chart.options);
                chart.buildTable();
                chart.writeLatex();
                this.charts.push(chart);
            }
        }
    }
}

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("../xml/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 = "../php/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.getRawScoreData = {
        root: document.createElement("div"),
        csvDOM: document.createElement("button"),
        jsonDOM: document.createElement("button"),
        xmlDOM: document.createElement("button"),
        presentDOM: document.createElement("div"),
        parent: this,
        XHR: new XMLHttpRequest(),
        handleEvent: function(event) {
            this.presentDOM.innerHTML = null;
            var url = "../php/get_filtered_score.php"+this.parent.getFilterString();
            this.XHR.open("GET",url+"&format="+event.currentTarget.textContent,true);
            switch(event.currentTarget.textContent) {
                case "CSV":
                    this.XHR.onload = function() {
                        var file = [this.response];
                        var bb = new Blob(file,{type: 'text/csv'});
                        this.parent.presentDOM.appendChild( this.parent.generateLink(bb,"scores.csv") );
                    }
                    break;
                case "JSON":
                    this.XHR.onload = function() {
                        var file = [this.response];
                        var bb = new Blob(file,{type: 'application/json'});
                        this.parent.presentDOM.appendChild( this.parent.generateLink(bb,"scores.json") );
                    }
                    break;
                case "XML":
                    this.XHR.onload = function() {
                        var file = [this.response];
                        var bb = new Blob(file,{type: 'text/xml'});
                        this.parent.presentDOM.appendChild( this.parent.generateLink(bb,"scores.xml") );
                    }
                    break;
            }
            this.XHR.send();
        },
        generateLink: function(blob,filename) {
            var dnlk = window.URL.createObjectURL(blob);
            var a = document.createElement("a");
            a.hidden = '';
            a.href = dnlk;
            a.download = filename;
            a.textContent = "Save File";
            return a;
        }
    }
    
    this.getRawScoreData.root.appendChild(this.getRawScoreData.csvDOM);
    this.getRawScoreData.root.appendChild(this.getRawScoreData.jsonDOM);
    this.getRawScoreData.root.appendChild(this.getRawScoreData.xmlDOM);
    this.getRawScoreData.root.appendChild(this.getRawScoreData.presentDOM);
    this.getRawScoreData.XHR.parent = this.getRawScoreData;
    this.getRawScoreData.csvDOM.textContent = 'CSV';
    this.getRawScoreData.csvDOM.addEventListener('click',this.getRawScoreData);
    this.getRawScoreData.jsonDOM.textContent = 'JSON';
    this.getRawScoreData.jsonDOM.addEventListener('click',this.getRawScoreData);
    this.getRawScoreData.xmlDOM.textContent = 'XML';
    this.getRawScoreData.xmlDOM.addEventListener('click',this.getRawScoreData);
    
    this.testSaves = {
        json: null,
        selectedURL: null,
        inputs: [],
        parent: this
    };
    this.init = function() {
        var self = this;
        get('../php/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;
            }
        }
        var options = [];
        if(specification.preTest) {
            options = options.concat(specification.preTest.options);
        }
        if (specification.postTest) {
            options = options.concat(specification.postTest.options);
        }
        for (var survey_entry of 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);
        document.getElementById("test-saved").appendChild(this.getRawScoreData.root);
    }
    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 = "../php/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();
}