Skip to content
Snippets Groups Projects
basynthec-browser.js 33.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • // The width of the visualization
    
    var curveColors = d3.scale.category10().domain([0, 9]);
    
    cramakri's avatar
    cramakri committed
    
    
    /**
     * Abstract superclass for the wrapper classes.
     * @constructor
     */
    function AbstractThingWrapper() {
    	this.isDataSet = function() { return false; }
    	this.isStrain = function() { return false; }
    }
    
    /**
     * Create a wrapper on a data set designed to be displayed as a data set.
     * 
     * @constructor
     */
    function DataSetWrapper(dataSet) {
    	var strainNames;
    	if (dataSet.properties[STRAIN_PROP_NAME] != null)
    		strainNames = dataSet.properties[STRAIN_PROP_NAME].split(",");
    	else
    		strainNames = [dataSet.properties["STRAIN_NAME"]];
    				
    	this.strainNames = strainNames;
    	this.dataSet = dataSet;
    	this.dateString = timeformat(new Date(dataSet.registrationDetails.registrationDate));
    	this.strainString = 
    							(strainNames.length < 3) ?
    								strainNames.join(" ") :
    								"" + strainNames.length + " strain(s)";
    	this.userEmail = dataSet.registrationDetails.userEmail;
    	this.name = dataSet.code;
    }
    
    DataSetWrapper.prototype = new AbstractThingWrapper();
    DataSetWrapper.prototype.constructor = DataSetWrapper;
    DataSetWrapper.prototype.isDataSet = function() { return true; }
    
    /**
     * Create a wrapper that represents a strain to be displayed
     *
     * @constructor
     */
    function StrainWrapper(strainName)
    {
    	this.strainName = strainName;
    	this.dataSets = [];
    }
    
    StrainWrapper.prototype = new AbstractThingWrapper();
    StrainWrapper.prototype.constructor = StrainWrapper;
    StrainWrapper.prototype.isStrain = function() { return true; }
    
    
    var presenterModeTypeDataSet = "DATA_SET", presenterModeTypeStrain = "STRAIN";
    
    
    /**
     * An object responsible for managing the view
     */ 
    function AppPresenter() {
    
    	this.didCreateVis = false;
    
    	this.presenterMode = presenterModeTypeDataSet;
    	this.visualizationContainers = [];
    
    }
    
    /** Hides the explanation and shows the element to display the explanation again */
    AppPresenter.prototype.hideExplanation = function() {
    	$('#explanation').hide();
    	$('#explanation-show').show();	
    }
    
    /** Display the explanation again */
    AppPresenter.prototype.showExplanation = function() {
    	$('#explanation-show').hide();		
    	$('#explanation').show();
    }
    
    /** Show the data sets grouped by type */
    AppPresenter.prototype.switchToDataSetTypeView = function()
    {
    
    	this.presenterMode = presenterModeTypeDataSet;
    
    	this.hideExplanation();
    
    	this.toggleDisplayedVisualizations(dataSetTypeVis);
    
    	od600InspectorView.removeAll(250);
    	dataSetInspectorView.updateView();	
    
    }
    
    /** Show the data sets by strain*/
    AppPresenter.prototype.switchToStrainView = function()
    {
    
    	this.presenterMode = presenterModeTypeDataSet;
    
    	this.hideExplanation();
    
    	this.toggleDisplayedVisualizations(strainVis);
    
    	od600InspectorView.removeAll(250);	
    	dataSetInspectorView.updateView();		
    
    }
    
    /** Show the data sets by strains with OD600 data*/
    AppPresenter.prototype.switchToOD600View = function()
    {
    
    	this.presenterMode = presenterModeTypeStrain;
    
    	this.hideExplanation();
    
    	this.toggleDisplayedVisualizations(od600StrainVis);
    	dataSetInspectorView.removeAll(250);
    	od600InspectorView.updateView();
    
    /** This view is very similar to the OD600 view, but the data is also divided into two main groups:
     * - strains for which there are phenotypes, predictions, and data in the openBIS database
     * - strains for which there are phenotypes or predictions, but no data in the openBIS database
     *   (in this group strains with phenotypes and predictions should be marked green; strains with 
     *   phenotypes only should be marked blue; strains with predictions only should be yellow) 
     * Information about the phenotypes and predictions is retrieved from UChicago strain database
     * (http://pubseed.theseed.org/model-prod/StrainServer.cgi) and cached at the server-side in OpenBIS.
     */
    AppPresenter.prototype.switchToOD600WithPhenotypesAndPredictionsView = function()
    {
    	this.presenterMode = presenterModeTypeStrain;
    	this.hideExplanation();
    	this.toggleDisplayedVisualizations(od600StrainWithPhenotypesAndPredictionsVis);
    	dataSetInspectorView.removeAll(250);
    	od600InspectorView.updateView();
    }
    
    
    /** Utility function to gracefully switch from one visualization to another */
    
    AppPresenter.prototype.toggleDisplayedVisualizations = function(visToShow)
    
    	this.visualizationContainers.forEach(function(vis) {
    		if (vis == visToShow) {
    
    			if (od600StrainVis == vis || od600StrainWithPhenotypesAndPredictionsVis == vis) {
    
    				// So that scrolling works
    				vis.style("display", "block");
    			} else {
    				vis.style("display", "inline");
    			}
    			vis
    
    				.transition()
    			.duration(1000)
    			.style("opacity", 1);
    		} else {
    
    			// change to "inline" element to eliminate jumping of two "block" elements during transition
    			vis.style("display","inline");
    
    			vis
    				.transition()
    			.duration(1000)
    			.style("opacity", 0)
    			.style("display", "none")
    		}
    	});
    
    }
    
    /**
     * Shows the list of data sets retrieved from the openBIS server.
     */
    AppPresenter.prototype.showDataSets = function(bisDataSets) {
    	if (null == bisDataSets) return;
    	
    	basynthec.dataSetList = bisDataSets.filter(function(dataSet) { 
    		return IGNORED_DATASET_TYPES.indexOf(dataSet.dataSetTypeCode) == -1;
      });
    	
    	// sort data sets
    	var sortByTypeAndRegistration = function(a, b) {
    		if (a.dataSetTypeCode == b.dataSetTypeCode) {
    			return b.registrationDetails.registrationDate - a.registrationDetails.registrationDate;
    		}
    		return (a.dataSetTypeCode < b.dataSetTypeCode) ? -1 : 1;
    	};
    	
    	basynthec.dataSetList.sort(sortByTypeAndRegistration);
    
    
    	model.initialize(function(){
    		presenter.refreshDataSetTypeTables();
    		presenter.refreshStrainTables();
    		presenter.refreshOd600StrainTables();
    		presenter.refreshOd600StrainWithPhenotypesAndPredictionsTables();
    	});
    
    }
    
    AppPresenter.prototype.refreshStrainTables = function() {
      this.createVis();
     	strainView.updateView(1000);
    }
    
    AppPresenter.prototype.refreshDataSetTypeTables = function() {
      this.createVis();
    
    	od600View.updateView();
    	metabolomicsView.updateView();
    	transcriptomicsView.updateView();
    	proteomicsView.updateView();
    
    AppPresenter.prototype.refreshOd600StrainTables = function() {
      this.createVis();
     	od600StrainView.updateView(1000);
    }
    
    
    AppPresenter.prototype.refreshOd600StrainWithPhenotypesAndPredictionsTables = function() {
      this.createVis();
      od600StrainWithPhenotypesAndPredictionsView.updateView(1000);
    }
    
    
    
    AppPresenter.prototype.createVis = function()
    {
    	if (this.didCreateVis) return;
    	
    	var top = d3.select("#main");
    	var tableRoot = top.append("div").attr("id", "table-root").style("width", w + 5 + "px").style("float", "left");
    	
    	
    	// An element for the inspectors.
    	inspectors = top.append("span")
    		.style("width", "500px")
    		.style("position", "relative")
    		.style("overflow", "auto")
    		.style("float", "left")
    		.style("left", "20px");
    
    	// Create the dataSetType visualization
    	dataSetTypeVis = tableRoot.append("div");
    	dataSetTypeVis.style("width", w + "px");
    	od600View = new DataSummaryView(dataSetTypeVis, "OD600", "od600", "OD600");
    	metabolomicsView = new DataSummaryView(dataSetTypeVis, "Metabolomics", "metabolomics", "METABOLITE_INTENSITIES");
    	transcriptomicsView = new DataSummaryView(dataSetTypeVis, "Transcriptomics", "transcriptomics", "TRANSCRIPTOMICS");
    	proteomicsView = new DataSummaryView(dataSetTypeVis, "Proteomics", "proteomics", "PROTEIN_QUANTIFICATIONS");
    
    	sequencesView = new DataSummaryView(dataSetTypeVis, "Sequences", "sequences", "SEQUENCE_CM5");
    
    	
    	// Initially hide the strain view -- it is activated by the radio button
    	strainVis = tableRoot.append("div").style("display", "none");
    	strainVis.style("width", w + "px");
    
    	strainView = new StrainView();
    	
    
    	od600StrainVis = tableRoot.append("div").style("display", "none");
    	od600StrainVis.style("width", w + "px");
    
    	od600StrainVis.style("height",w + "px");
    
    	od600StrainVis.style("overflow-y", "scroll");
    	od600StrainVis.style("opacity", "0");
    
    	od600StrainView = new Od600StrainView();
    	
    
    	od600StrainWithPhenotypesAndPredictionsVis = tableRoot.append("div").style("display", "none");
    	od600StrainWithPhenotypesAndPredictionsVis.style("width", w + "px");
    	od600StrainWithPhenotypesAndPredictionsVis.style("height",w + "px");
    
    	od600StrainWithPhenotypesAndPredictionsVis.style("overflow-y", "scroll");
    	od600StrainWithPhenotypesAndPredictionsVis.style("opacity", "0");
    
    	od600StrainWithPhenotypesAndPredictionsView = new Od600StrainWithPhenotypesAndPredictionsView();
    	
    	this.visualizationContainers = [dataSetTypeVis, strainVis, od600StrainVis, od600StrainWithPhenotypesAndPredictionsVis];
    
    	dataSetInspectorView = new DataSetInspectorView();
    	od600InspectorView = new Od600InspectorView();
    
    	
    	this.didCreateVis = true;
    }
    
    AppPresenter.prototype.updateInspectors = function(duration) 
    {
    
    	if (this.presenterMode == presenterModeTypeDataSet)
    	{
    
    		dataSetInspectorView.updateView(duration);
    
    		od600InspectorView.updateView(duration);
    
    /** Download a file referenced in a table. */
    AppPresenter.prototype.downloadTableFile = function(d)
    {
    	// If there is no dataset, this is just a marker for loading
    	if (!d.dataset) return;
    	
    	var action = function(data) { 
    		try {
    			document.location.href = data.result
    		} catch (err) {
    			// just ignore errors		
    		} 
    	};
    	basynthec.server.getDownloadUrlForFileForDataSet(d.dataset.bis.code, d.pathInDataSet, action);
    }
    
    AppPresenter.prototype.toggleInspected = function(d, connectedNode) {
    	this.hideExplanation();
    
    
    	var lexicalParent = this;
    	
    	function retrieveFilesForDataSets(dataSets) { 
    		dataSets.forEach(function(ds) { lexicalParent.retrieveFilesForDataSet(ds); });
    	}
    
    	
    	function classForNode(d) { return  (d.inspected) ? "inspected" : "" };
    
    	if (d.inspected) {
    		var index = inspected.indexOf(d) 
    		if (index > -1)	inspected.splice(index, 1);
    		d.inspected = false;
    	} else {
    		d.inspected = true;
    		d.connectedNode = connectedNode;
    		inspected.push(d);
    
    		if (d instanceof DataSetWrapper) {
    
    			d.dataSets = [{ bis : d.dataSet }];
    			retrieveFilesForDataSets(d.dataSets);
    		} else if (!d.dataSets) {
    			d.dataSets = model.dataSetsByStrain[d.name].dataSets.map(function(ds){ return {bis : ds} }); 
    			retrieveFilesForDataSets(d.dataSets);
    		}
    	}
    	
    	d3.select(d.connectedNode).attr("class", classForNode(d))
      this.updateInspectors(500);
    }
    
    
    AppPresenter.prototype.toggleOd600Inspected = function(d) {
    
    	this.hideExplanation();
    
    	var lexicalParent = this;
    	
    	function retrieveFilesForDataSets(dataSets) { 
    		dataSets.forEach(function(ds) { lexicalParent.retrieveFilesForDataSet(ds); });
    	}
    	
    	function classForNode(d) { return  (d.od600Inspected) ? "inspected" : "" };
    
    
    	var inspectedItem = null;
    	var inspectedIndex = null;
    	
    	$.each(od600Inspected, function(index, item){
    		if(item.name == d.name){
    			inspectedItem = item;
    			inspectedIndex = index;
    			return false;
    		}
    	});
    	
    	if (inspectedItem) {
    		od600Inspected.splice(inspectedIndex, 1);
    
    	} else {
    		od600Inspected.push(d);
    		if (d instanceof DataSetWrapper) {
    			d.dataSets = [{ bis : d.dataSet }];
    			retrieveFilesForDataSets(d.dataSets);
    		} else if (!d.dataSets) {
    			d.dataSets = model.dataSetsByStrain[d.name].dataSets.map(function(ds){ return {bis : ds} }); 
    			retrieveFilesForDataSets(d.dataSets);
    		}
    	}
    	
    
        this.updateInspectors(500);
        od600StrainView.updateView(1);
        od600StrainWithPhenotypesAndPredictionsView.updateView(1);
    
    AppPresenter.prototype.retrieveFilesForDataSet = function(ds)
    {
    	if (ds.files) {
    		// already retrieved
    		return;
    	}
    	
    	ds.loadingFiles = true;
    	ds.files = [];
    
    
    	basynthec.server.listFilesForDataSet(ds.bis.code, "/", true, function(data) {					
    		if (!data.result) { 
    			return;
    		}
    		data.result.forEach(function (file) { file.dataset = ds });
    		ds.files = ds.files.concat(data.result);
    		
    		ds.loadingFiles = false; 
    		presenter.updateInspectors(500);
    		
    		if (isOd600DataSet(ds)) {
    
    			lexicalParent.retrieveOd600DataForDataSet(ds)
    
    /** Load the OD600 data from the server. This function assumes that the files are already known. */
    AppPresenter.prototype.retrieveOd600DataForDataSet = function(ds)
    {
    	if (ds.od600Rows) {
    		// already retrieved
    		return;
    	}
    	
    	ds.loadingOd600 = true;
    	ds.od600Rows = [];
    	
    	// Figure out the path to the multistrain TSV file -- this path ends with "xls.tsv".
    	var tsvPathInDataSet = "";
    	ds.files.forEach(function (file) { if (endsWith(file.pathInDataSet, "xls.tsv")) tsvPathInDataSet = file.pathInDataSet});
    		
    	var tsvUrl = dssUrl + "/" + ds.bis.code + "/" + tsvPathInDataSet + "?sessionID=" + basynthec.server.sessionToken;
    
    
    	d3.text(tsvUrl, "text/tsv", function(text) {
    		var rows = d3.tsv.parseRows(text, "\t");
    		ds.od600Rows = rows;
    
    		lexicalParent.initializeOd600Map(ds);
    		ds.loadingOd600 = false;
    
    		presenter.updateInspectors(500);
    	});	
    }
    
    
    /** 
     * Initialize a map of the OD600 data, where the key is the strain and the value is an array 
     * 
     * Assumes that ds.od600Rows has already been set
     */
    AppPresenter.prototype.initializeOd600Map = function(ds) 
    {
    	ds.od600Map = {};
    	// The first line of data contains the timepoints
    	ds.od600Timepoints = ds.od600Rows[0].slice(2);
    	var i;
    	for (i = 1; i < ds.od600Rows.length; ++i) {
    		var line = ds.od600Rows[i];
    		var strain = line[0].toUpperCase();
    
    		var data = line.slice(2).map(function(str) { return parseFloat(str)});
    
    		if (null == ds.od600Map[strain]) ds.od600Map[strain] = [];
    		ds.od600Map[strain].push(data);
    
    /**
     * An object responsible for managing the data to show
     */
    function AppModel() {
    	// a map holding data sets by strain
    	this.dataSetsByStrain = { };
    
    	// Groups of strains to be displayed  
    	this.strainGroups = [];
    
    	// A map holding data sets by type
    	this.dataSetsByType = { };
    
    AppModel.prototype.initialize = function(callback) {
    
    	this.initializeDataSetsByType();
    	this.initializeDataSetsByStrain();
    	this.initializeStrainGroups();
    
    	this.initializeOd600Model();
    	this.initializeOd600WithPhenotypesAndPredictionsModel(callback);
    
    /** Compute the dataSetsByType variable */
    
    AppModel.prototype.initializeDataSetsByType = function() {
    
    	this.dataSetsByType = basynthec.dataSetList.reduce(
    
    		function(result, dataSet) {
    			var listForType = result[dataSet.dataSetTypeCode];
    			if (listForType == null) {
    				listForType = [];
    				result[dataSet.dataSetTypeCode] = listForType;
    			}
    			listForType.push(new DataSetWrapper(dataSet));
    			return result;
    		}, {});
    }
    
    /** Compute the dataSetsByStrain variable */
    
    AppModel.prototype.initializeDataSetsByStrain = function() {
    
    	this.dataSetsByStrain = basynthec.dataSetList.reduce(
    
    		function(result, dataSet) { 
    			var uniqueStrains = uniqueElements(basynthec.getStrains(dataSet).sort());
    			
    			uniqueStrains.forEach(function(strain) {
    					if (!result[strain]) {
    						result[strain] = new StrainWrapper(strain);
    					}
    					result[strain].dataSets.push(dataSet);
    			});
    			return result;
    		}, {});
    }
    
    
    AppModel.prototype.initializeStrainGroups = function() {
    	var strains = []
    	for (strainName in this.dataSetsByStrain) {
    
    		strains.push({name: strainName});
    
    	}
    	this.strainGroups = createStrainGroups(strains);
    
    /** Initialize the state necessary for the OD600 visualization. This must be run after initializeDataSetsByType */
    AppModel.prototype.initializeOd600Model= function() {
    	var colors = d3.scale.category20c();
    	var dataSets = this.od600DataSets();
    	var od600ExperimentIdentifiers = dataSets.map(function(ds) {
    		return ds.dataSet.experimentIdentifier;
    	});
    	
    	od600ExperimentIdentifiers.sort();
    	this.od600Experiments = od600ExperimentIdentifiers.map(function(expId, i) {
    		return { identifier: expId, color: colors(i) };
    	});
    	
    	// Filter the strain groups to those that have OD600 data
    	this.od600StrainGroups = this.strainGroups.map(function(group) {
    		var strains = group.strains.filter(function(strain) {
    			return model.dataSetsByStrain[strain.name].dataSets.some(function(ds) {
    				return "OD600" == ds.dataSetTypeCode;
    			})
    		});
    		return {groupName : group.groupName, strains : strains };
    	});
    }
    
    
    AppModel.prototype.initializeOd600WithPhenotypesAndPredictionsModel = function(callback){
    	var model = this;
    	
    	basynthec.getStrainsPhenotypesAndPredictions(function(strainDataMap){
    		
    		var strainsKnownToOpenbisWithPhenotypesAndPredictions = [];
    		var strainsUnknownToOpenbisWithPhenotypesOrPredictions = [];
    		
    		for(strainName in model.dataSetsByStrain){
    			var strainData = strainDataMap[strainName];
    			var strainDatasets = model.dataSetsByStrain[strainName];
    			
    			var hasPhenotypesAndPredictions = strainData && strainData.hasPhenotypes && strainData.hasPredictions;
    		    var hasOd600Datasets = strainDatasets && strainDatasets.dataSets.some(function(dataset){
    		    	return "OD600" == dataset.dataSetTypeCode;
    		    });
    			
    			if(hasPhenotypesAndPredictions && hasOd600Datasets){
    				strainData.isKnown = true;
    				strainsKnownToOpenbisWithPhenotypesAndPredictions.push(strainData);
    			}
    		}
    		
    		for(strainName in strainDataMap){
    			var strainData = strainDataMap[strainName];
    			var strainDatasets = model.dataSetsByStrain[strainName];
    			
    			var hasPhenotypesOrPredictions = strainData && (strainData.hasPhenotypes || strainData.hasPredictions);
    			var hasDatasets = strainDatasets && strainDatasets.dataSets.length > 0;
    			
    			if(hasPhenotypesOrPredictions && !hasDatasets){
    				strainData.isKnown = false;
    				strainsUnknownToOpenbisWithPhenotypesOrPredictions.push(strainData);
    			}
    		}
    		
    		model.od600StrainsWithPhenotypesAndPredictionsGroups = [];
    		model.od600StrainsWithPhenotypesAndPredictionsGroups.push({
    			"mainGroupName" : "Known strains with phenotypes and predictions",
    			"groups" : createStrainGroups(strainsKnownToOpenbisWithPhenotypesAndPredictions)
    		});
    		model.od600StrainsWithPhenotypesAndPredictionsGroups.push({
    			"mainGroupName" : "Unknown strains with phenotypes or predictions",
    			"groups" : createStrainGroups(strainsUnknownToOpenbisWithPhenotypesOrPredictions)
    		});
    		
    		callback();
    	});
    	
    }
    
    
    AppModel.prototype.od600DataSets = function() {
    	return this.dataSetsByType["OD600"];
    }
    
    
    /**
     * A utility function that groups strains together based on strain name
     */ 
    
    function createStrainGroups(strains) {
    
    	if(!strains || strains.length == 0){
    		return [];
    	}
    	
    
    	// prefixes of strain names to be grouped togehter
    	var STRAIN_GROUP_PREFIXES = [ "JJS-DIN", "JJS-MGP" ];
    
    	// The names to show the user for the strain groups
    	var STRAIN_GROUP_PREFIXES_DISPLAY_NAME = {"JJS-DIN" : "JJS-DIn", "JJS-MGP" : "JJS-MGP" };
    
    	
    	var groups = STRAIN_GROUP_PREFIXES.map(
    			function(strainPrefix) {
    				var filtered = strains.filter(function(strain) { 
    
    			    return strain.name.indexOf(strainPrefix) >= 0
    
    			  });
    				var groupStrains = filtered.map(function(strain) {
    
    					return { name : strain.name, label : strain.name.substring(strainPrefix.length), data: strain};
    
    			  });
    				
    				return {groupName : STRAIN_GROUP_PREFIXES_DISPLAY_NAME[strainPrefix], strains : groupStrains};
    	});
    	
    	var otherStrains = strains.filter(function(strain) {
    
          return false == STRAIN_GROUP_PREFIXES.some(function(prefix) { return strain.name.indexOf(prefix) >=0; } );
    
    	otherStrains = otherStrains.map(function(strain) { return {name:strain.name, label:strain.name, data: strain}});
    
    	groups.push({groupName : "Other strains", strains : otherStrains});
    	
    	var sortFunction = sortByProp("name")
    	groups.forEach(function(group) { group.strains.sort(sortFunction); });
    
    	// only return groups that have strains
    	return groups.filter(function(group) { return group.strains.length > 0 });
    }
    
    
     * View that groups data sets by type.
    
    function DataSummaryView(group, typeName, id, type) {
    	this.group = group;
    	this.dataSetTypeName = typeName;
    	this.dataSetType = type;
    	this.nodeId = id;
    	this.viewNode = this.createDataSetSummaryViewNode();
    
    DataSummaryView.prototype.createDataSetSummaryViewNode = function()
    
    	container = this.group.append("div");
    
    	container.append("h2").attr("class", "datasetsummarytable").text(this.dataSetTypeName);
    
    			.attr("id", this.nodeId)
    
    			.attr("class", "datasummaryview");
    			
    	return result;
    }
    
    
    DataSummaryView.prototype.updateView = function()
    
    	var dataSetsForType = model.dataSetsByType[this.dataSetType];
    
    	if (dataSetsForType == null) {
    		this.viewNode.selectAll("p")
    			.data(["No Data"])
    		.enter()
    			.append("p")
    			.text("No Data");
    		return;
    	}
    
    	this.viewNode.selectAll("table")
    		.data([dataSetsForType])
    	.enter()
    		.append("table")
    		.attr("class", "datasetsummarytable")
    		.selectAll("tr")
    			.data(function (d) { return d})
    		.enter()
    			.append("tr")
    
    			.on("click", toggleInspected)
    
    				.selectAll("td")
    					.data(function (d) { return [d.dateString, d.userEmail, d.strainString] })
    				.enter()
    					.append("td")
    					.style("width", "33%")
    					.text(function (d) { return d});
    
    function toggleInspected(d) { presenter.toggleInspected(d, this) }
    
    
    StrainView.prototype.updateView = function(duration)
    
    	var strainDiv = strainVis.selectAll("div.strains").data(model.strainGroups)
    
    		.enter()
    	.append("div")
    		.attr("class", "strains");
    
    	strainDiv
    		.append("h2")
    			.text(function(d) { return d.groupName });
    	strainDiv
    		.append("table")
    			.selectAll("tr").data(function(d) { 
    					// Group the different sets of strains differently
    					if (d.groupName.indexOf("Other") == 0) return d.strains.reduce(groupBy(3), []);
    					if (d.groupName.indexOf("JJS-MGP") == 0) return d.strains.reduce(groupBy(10), []);
    				
    					// Group the JJS-DIn strains by runs
    					return d.strains.reduce(groupByRuns(10), []) })
    				.enter()
    			.append("tr")
    			.selectAll("td").data(function(d) { return d })
    				.enter()
    			.append("td")
    
    			.on("click", toggleInspected)
    
    			.text(function(d) { return d.label });
    }
    
    
    function toggleOd600Inspected(d) { presenter.toggleOd600Inspected(d, this) }
    
    function isOd600Inspected(d) {
    	return od600Inspected.some(function(elem, index){
    		return elem.name == d.name;
    	});
    }
    
    
    /**
     * The view for the OD600 strains
     *
     * @constructor
     */
    function Od600StrainView() {
    	
    }
    
    Od600StrainView.prototype.updateView = function(duration)
    {
    	var strainDiv = od600StrainVis.selectAll("div.strains").data(model.od600StrainGroups)
    
    	strainDiv.enter().append("div").attr("class", "strains").append("h2").text(function(d) { 
    		return d.groupName 
    	})
    	
    	var tables = strainDiv.selectAll("table").data(function(d){
    		return [d];
    	})
    	tables.enter().append("table");
    	
    	var trs = tables.selectAll("tr").data(function(d) {
    
    					// Group the different sets of strains differently
    					if (d.groupName.indexOf("Other") == 0) return d.strains.reduce(groupBy(3), []);
    					if (d.groupName.indexOf("JJS-MGP") == 0) return d.strains.reduce(groupBy(10), []);
    				
    					// Group the JJS-DIn strains by runs
    					return d.strains.reduce(groupByRuns(10), []) })
    
        trs.enter().append("tr")
    			
        var tds = trs.selectAll("td").data(function(d) { return d });
    	tds.enter()
    
    			.append("td")
    			.on("click", toggleOd600Inspected)
    
    			.text(function(d) { return d.label })
    	tds.attr("class", function(d){
    		if(isOd600Inspected(d)){
    			return "inspected"
    		}else{
    			return "";
    		}
    	})
    
    /**
     * The view for the OD600 strains with phenotypes and predictions
     *
     * @constructor
     */
    function Od600StrainWithPhenotypesAndPredictionsView(){
    }
    
    Od600StrainWithPhenotypesAndPredictionsView.prototype.updateView = function(duration)
    {
    
    	var mainGroupDiv = od600StrainWithPhenotypesAndPredictionsVis.selectAll("div.strainsMainGroup").data(model.od600StrainsWithPhenotypesAndPredictionsGroups);
    	mainGroupDiv.enter().append("div").attr("class", "strainsMainGroup").append("h2").text(function(d) { 
    		return d.mainGroupName 
    	});	
    
    	var strainDiv = mainGroupDiv.selectAll("div.strains").data(function(d){ 
    		return d.groups; 
    	})
    	strainDiv.enter().append("div").attr("class", "strains").append("h3").text(function(d) { 
    		return d.groupName 
    	})
    
    	var tables = strainDiv.selectAll("table").data(function(d){
    		return [d];
    	})
    	tables.enter().append("table");
    
    	var trs = tables.selectAll("tr").data(function(d) {
    
    					// Group the different sets of strains differently
    					if (d.groupName.indexOf("Other") == 0) return d.strains.reduce(groupBy(3), []);
    					if (d.groupName.indexOf("JJS-MGP") == 0) return d.strains.reduce(groupBy(10), []);
    				
    					// Group the JJS-DIn strains by runs
    					return d.strains.reduce(groupByRuns(10), []) })
    
        trs.enter().append("tr")
    			
        var tds = trs.selectAll("td").data(function(d) { return d });
    	tds.enter()
    
    			.append("td")
    			.on("click", function(d){
    				if(d.data.isKnown){
    
    				}
    			})
    			.text(function(d) { return d.label })
    			.style("color", function(d){
    				if(d.data.isKnown){
    					return "black";
    				}else{
    					if(d.data.hasPhenotypes && d.data.hasPredictions){
    						return "green";
    					}else if(d.data.hasPhenotypes){
    						return "blue";
    					}else if(d.data.hasPredictions){
    						return "red";
    					}
    				}
    
    			})
    	tds.attr("class", function(d){
    		var classes = [];
    		
    		if(isOd600Inspected(d)){
    			classes.push("inspected");
    		}
    		if(!d.data.isKnown){
    			classes.push("unknown");
    		}
    		
    		return classes.join(" ");
    	})
    
    	var legend = od600StrainWithPhenotypesAndPredictionsVis.selectAll("div.legend").data([model.od600StrainsWithPhenotypesAndPredictionsGroups]);
    	legend.enter().append("div").attr("class","legend").append("h3").style("font-style","italic").text("Legend");
    
    	var legendList = legend.selectAll("ul").data(function(d){ return [d] }).enter().append("ul");
    
    	legendList.append("li").append("span").text("strain with phenotypes and predictions").style("color","green");
    	legendList.append("li").append("span").text("strain with phenotypes only").style("color","blue");
    	legendList.append("li").append("span").text("strain with predictions only").style("color","red");
    }
    
    
    /**
     * The view that shows data sets.
     *
     * @constructor
     */
    function DataSetInspectorView() {
    
    DataSetInspectorView.prototype.updateView = function(duration)
    
    {	
    	var inspector = inspectors.selectAll("div.inspector").data(inspected, function (d) { return d.name });
    	
    	var box = inspector.enter().append("div")
    		.attr("class", "inspector")
    		.text(function(d) { return d.name });
    
    	box.append("span")
    		.attr("class", "close")
    
    		.on("click", toggleInspected)
    
    		.text("x");
    	
    	var dataSetList = inspector.selectAll("ul").data(function (d) { return [d] });
    	dataSetList.enter()
    	  .append("ul")
    	  .attr('class', 'dataSets');
    	
    	
    	var dataSetElt = dataSetList.selectAll("li").data(function (d) { return d.dataSets });
    	dataSetElt.enter()
    	  .append("li")
    	  .text(function(d) { return dataSetLabel(d) });
    	
    	var dataSetDetailsElt = dataSetElt.selectAll("div.dataSetDetails").data(function(d) { return [d]; });
    	dataSetDetailsElt
    	  .enter()
    	    .append("div")
    	      .attr("class", "dataSetDetails"); 
    	
    	var propsTable = dataSetDetailsElt.selectAll("table.properties").data(function(d) {return [d]});
    	
    	propsTable.enter()
    	  .append("table")
    	  .attr("class", "properties");
    	
    	propsTable.selectAll("tr").data(function(d) { return props_to_pairs(d.bis.properties) })
    		.enter()
    			.append("tr")
    			.selectAll("td").data(function(d) { return d } ).enter()
    				.append("td")
    				.attr("class", function(d, i) { return (i == 0) ? "propkey" : "propvalue"})
    				.style("opacity", "0")
    				.text(function(d) { return d })
    			.transition()
    				.style("opacity", "1");
    	
    	var downloadTable = dataSetDetailsElt.selectAll("table.downloads").data(function(d) { return [d] });
    	
    	downloadTable
    		.enter()
    			.append("table")
    				.attr("class", "downloads")
    			
    	// Add a caption, but make sure there is just one (this does not work with select())
    	downloadTable.selectAll("caption").data(["Files"])
    		.enter()
    			.append("caption").text(function(d) { return d; });
    			
    	// We just want to see non-directories here
    	var downloadTableRow = downloadTable.selectAll("tr").data(filesForDataSet, function(d) { return d.pathInDataSet });
    	downloadTableRow
    		.enter()
    			.append("tr")
    				.append("td")
    				.on("click", downloadTableFile)
    				.text(function(d) { return d.pathInListing });
    	downloadTableRow
    		.exit()
    			.transition()
    				.duration(duration)
    				.style("opacity", "0")
    				.remove();
    	
    	inspector.exit().transition()
    		.duration(duration)
    		.style("opacity", "0")
    		.remove();
    }
    
    
    /** Removes all nodes from the view, without affecting the model */
    
    DataSetInspectorView.prototype.removeAll = function(duration) 
    
    {
    	var inspector = inspectors.selectAll("div.inspector").data([]);
    	inspector.exit().transition()
    		.duration(duration)
    		.style("opacity", "0")
    		.remove();
    }
    
    
    /**
     * The view that shows growth curves
     *
     * @constructor
     */
    function Od600InspectorView() {
    	
    }
    
    Od600InspectorView.prototype.updateView = function(duration)
    {	
    
    	var height = 100, width = 100;
    
    	var inspector = inspectors.selectAll("div.od600inspector").data(od600Inspected, function (d) { return d.name });
    	
    
    	var strainBox = inspector.enter().append("div")
    
    		.attr("class", "od600inspector")
    
    		.style("width", width + "px")
    		.style("float", "left")
    		.style("padding", "5px");
    
    	strainBox
    		.append("div")
    			.text(function(d) { return d.name })
    		.append("span")
    			.attr("class", "close")
    			.on("click", toggleOd600Inspected)
    			.text("x");
    		
    	strainBox.append("svg:svg")
    
    		.attr("width", width);	
    
    	var dataDisplay = inspector.select("svg").selectAll("g.curve").data(od600DataForStrain);
    	dataDisplay.enter()
    		.append("svg:g")
    			.attr("class", "curve");
    
    	dataDisplay = inspector.select("svg").selectAll("g.curve").data(od600DataForStrain);
    	var aCurve = dataDisplay.selectAll("g.lines").data(curveData)
    
    			.append("svg:g")
    				.attr("class", "lines");
    
    	aCurve = dataDisplay.selectAll("g.lines").data(curveData);
    
    		// The first two columns of data are the strain name and human-readable desc
    	aCurve.selectAll("line").data(lineData)
    		.enter()
    	.append("svg:line")
    		.attr("x1", function(d, i) { return (i / (this.parentNode.__data__.length)) * width; })
    		.attr("y1", function(d, i) { return height - (d[0] * height); })
    		.attr("x2", function(d, i) { return ((i + 1) / (this.parentNode.__data__.length)) * width;})
    		.attr("y2", function(d) { return height - (d[1] * height); })
    
    		.style("stroke", function(d) { return this.parentNode.__data__.color;})
    
    		.style("stroke-width", "1");
    	
    	inspector.exit().transition()
    		.duration(duration)
    		.style("opacity", "0")
    		.remove();
    }
    
    /** Removes all nodes from the view, without affecting the model */