From b7ed0edf2c95a6e04cd71330b0f6cfa36990dcd2 Mon Sep 17 00:00:00 2001 From: Aaron Ponti <aaron.ponti@bsse.ethz.ch> Date: Thu, 9 May 2019 08:45:37 -0700 Subject: [PATCH] Partial implementation of FCS data plotting in ELN-LIMS. --- .../js/plugins/FlowCytometryTechnology.js | 576 ++++++++++++++++++ 1 file changed, 576 insertions(+) diff --git a/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/plugins/FlowCytometryTechnology.js b/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/plugins/FlowCytometryTechnology.js index e4fe043dfb6..f9b757f7d6f 100644 --- a/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/plugins/FlowCytometryTechnology.js +++ b/openbis_standard_technologies/dist/core-plugins/eln-lims/1/as/webapps/eln-lims/html/js/plugins/FlowCytometryTechnology.js @@ -5,6 +5,11 @@ function FlowCytometryTechnology() { $.extend(FlowCytometryTechnology.prototype, ELNLIMSPlugin.prototype, { init: function () { + // Store a reference to the "retrieve FCS events" service + this.retrieveFCSEventsService = null; + + // Data cache + this.dataCache = {}; }, forcedDisableRTF: [], forceMonospaceFont: [], @@ -148,8 +153,579 @@ $.extend(FlowCytometryTechnology.prototype, ELNLIMSPlugin.prototype, { }, dataSetFormTop: function ($container, model) { + // Render the paremeter options + this.renderParameterSelectionWidget($container, model); + + // Append the div where the data will be plotted + $container.append($('<div>').attr("id", "plot_canvas_div")); + }, dataSetFormBottom: function ($container, model) { + }, + + // Additional functionality + renderParameterSelectionWidget: function ($container, model) { + + // Clear the container + $container.empty(); + + // Check that we ave the correct dataset type + if (!model.dataSetV3.type.code.endsWith("_FCSFILE")) { + return; + } + + // + // Retrieve the parameter info + // + var parameterInfo = this.retrieveParameterInfo(model); + + // Add legend + var legend = $("<legend>") + .text("Data viewer") + $container.append(legend); + + // Create a div for all plotting options + var plot_params_div = $('<div>') + .css("text-align", "left") + .css("margin", "5px 0 15px 0") + .attr("id", "plot_params_div"); + + // + // Lay out the widget + // + + // Create a form for the plot parameters + var form = $("<form>") + .attr("id", "parameter_form"); + plot_params_div.append(form); + + // Create divs to spatially organize the groups of parameters + var xAxisDiv = $("<div>") + .css("display", "inline-block") + .css("text-align", "right") + .attr("id", "xAxisDiv") + var yAxisDiv = $("<div>") + .css("display", "inline-block") + .css("text-align", "right") + .attr("id", "yAxisDiv") + var eventsDiv = $("<div>") + .css("display", "inline-block") + .css("text-align", "right") + .attr("id", "eventsDiv") + var plotDiv = $("<div>") + .css("display", "inline-block") + .css("vertical-align", "top") + .css("padding-left", "10px") + .attr("id", "plotDiv") + + // Add them to the form + form.append(xAxisDiv); + form.append(yAxisDiv); + form.append(eventsDiv); + form.append(plotDiv); + + // X axis parameters + xAxisDiv.append($("<label>") + .attr("for", "parameter_form_select_X_axis") + .html("X axis")); + var selectXAxis = $("<select>") + .css("margin", "0 3px 0 3px") + .attr("id", "parameter_form_select_X_axis"); + xAxisDiv.append(selectXAxis); + + // Y axis parameters + yAxisDiv.append($("<label>") + .attr("for", "parameter_form_select_Y_axis") + .html("Y axis")); + var selectYAxis = $("<select>") + .css("margin", "0 3px 0 3px") + .attr("id", "parameter_form_select_Y_axis"); + yAxisDiv.append(selectYAxis); + + // Add all options + for (var i = 0; i < parameterInfo.numParameters; i++) { + var name = parameterInfo["names"][i]; + var compositeName = parameterInfo["compositeNames"][i]; + selectXAxis.append($("<option>") + .attr("value", name) + .text(compositeName)); + selectYAxis.append($("<option>") + .attr("value", name) + .text(compositeName)); + } + + // // Pre-select some parameters + selectXAxis.val(parameterInfo["names"][0]); + selectYAxis.val(parameterInfo["names"][1]); + + // Add a selector with the number of events to plot + eventsDiv.append($("<label>") + .attr("for", "parameter_form_select_num_events") + .html("Events to plot")); + var selectNumEvents = $("<select>") + .css("margin", "0 3px 0 3px") + .attr("id", "parameter_form_select_num_events"); + eventsDiv.append(selectNumEvents); + + // Add the options + var possibleOptions = [500, 1000, 2500, 5000, 10000, 20000, 50000, 100000]; + var numEventsInFile = parseInt(parameterInfo.numEvents); + for (var i = 0; i < possibleOptions.length; i++) { + if (possibleOptions[i] < numEventsInFile) { + selectNumEvents.append($("<option>") + .attr("value", possibleOptions[i]) + .text(possibleOptions[i].toString())); + } + } + selectNumEvents.append($("<option>") + .attr("value", parameterInfo.numEvents) + .text(parseInt(parameterInfo.numEvents))); + + // Pre-select something reasonable + if (parameterInfo.numEvents > possibleOptions[4]) { + selectNumEvents.val(parseInt(possibleOptions[4])); + } else { + selectNumEvents.val(parseInt(parameterInfo.numEvents)); + } + + // Add "Plot" button + var thisObj = this; + var plotButton = $("<input>") + .attr("type", "button") + .attr("value", "Plot") + .click(function () { + + // Get the selected parameters and their display scaling + var paramX = selectXAxis.find(":selected").val(); + var paramY = selectYAxis.find(":selected").val(); + var displayX = selectScaleX.find(":selected").val(); + var displayY = selectScaleY.find(":selected").val(); + + // How many events to plot? + var numEventsToPlot = selectNumEvents.val(); + + // Sampling method + var samplingMethod = selectSamplingMethod.find(":selected").val(); + + // Call the retrieving and plotting method + thisObj.callServerSidePluginGenerateFCSPlot( + model, + paramX, + paramY, + displayX, + displayY, + numEventsToPlot, + parameterInfo.numEvents, + samplingMethod); + }); + plotDiv.append(plotButton); + + // Add a selector with the scaling for axis X + var xAxisScalingDiv = xAxisDiv.append($("<div>") + .css("display", "block") + .attr("id", "xAxisScalingDiv")); + xAxisScalingDiv.append($("<label>") + .attr("for", "parameter_form_select_scaleX") + .html("Scale for X axis")); + var selectScaleX = $("<select>") + .css("margin", "0 3px 0 3px") + .attr("id", "parameter_form_select_scaleX"); + xAxisScalingDiv.append(selectScaleX); + + // Add the options + possibleOptions = ["Linear", "Hyperlog"]; + for (var i = 0; i < possibleOptions.length; i++) { + selectScaleX.append($("<option>") + .attr("name", possibleOptions[i]) + .attr("value", possibleOptions[i]) + .text(possibleOptions[i])); + } + + // Pre-select "Linear" + $("parameter_form_select_scaleX").val(0); + + // Add a selector with the scaling for axis Y + var yAxisScalingDiv = yAxisDiv.append($("<div>") + .css("display", "block") + .attr("id", "yAxisScalingDiv")); + yAxisScalingDiv.append($("<label>") + .attr("for", "parameter_form_select_scaleY") + .html("Scale for Y axis")); + var selectScaleY = $("<select>") + .css("margin", "0 3px 0 3px") + .attr("id", "parameter_form_select_scaleY"); + yAxisScalingDiv.append(selectScaleY); + + // Add the options + possibleOptions = ["Linear", "Hyperlog"]; + for (var i = 0; i < possibleOptions.length; i++) { + selectScaleY.append($("<option>") + .attr("name", possibleOptions[i]) + .attr("value", possibleOptions[i]) + .text(possibleOptions[i])); + } + + // Pre-select "Linear" + $("parameter_form_select_scaleY").val(0); + + // Add a selector with the sampling method + var eventSamplingDiv = eventsDiv.append($("<div>") + .css("display", "block") + .attr("id", "eventSamplingDiv")); + eventSamplingDiv.append($("<label>") + .attr("for", "parameter_form_select_sampling_method") + .html("Sampling")); + var selectSamplingMethod = $("<select>") + .css("margin", "0 3px 0 3px") + .attr("id", "parameter_form_select_sampling_method"); + eventSamplingDiv.append(selectSamplingMethod); + + // Add the options + possibleOptions = ["Regular", "First rows"]; + for (var i = 0; i < possibleOptions.length; i++) { + selectSamplingMethod.append($("<option>") + .attr("name", "" + (i + 1)) + .attr("value", (i + 1)) + .text(possibleOptions[i])); + } + + // Pre-select "Linear" + $("parameter_form_select_sampling_method").val(0); + + // + // End of widget + // + + // Append the created div to the container + $container.append(plot_params_div); + + }, + + retrieveParameterInfo: function (model) { + + // Retrieve parameter information + var key = model.dataSetV3.type.code.substring( + 0, model.dataSetV3.type.code.indexOf("_FCSFILE")) + + "_FCSFILE_PARAMETERS"; + + var parametersXML = $.parseXML(model.dataSetV3.properties[key]); + var parameters = parametersXML.childNodes[0]; + + var numParameters = parameters.getAttribute("numParameters"); + var numEvents = parameters.getAttribute("numEvents"); + + var names = []; + var compositeNames = []; + var display = []; + + // Parameter numbering starts at 1 + var parametersToDisplay = 0; + for (var i = 1; i <= numParameters; i++) { + + // If the parameter contains the PnCHANNELTYPE attribute (BD Influx Cell Sorter), + // we only add it if the channel type is 6. + var channelType = parameters.getAttribute("P" + i + "CHANNELTYPE"); + if (channelType != null && channelType !== 6) { + continue; + } + + // Store the parameter name + var name = parameters.getAttribute("P" + i + "N"); + names.push(name); + + // Store the composite name + var pStr = parameters.getAttribute("P" + i + "S"); + var composite = name; + if (pStr !== "") { + composite = name + " (" + pStr + ")"; + } + compositeNames.push(composite); + + // Store the display scale + var displ = parameters.getAttribute("P" + i + "DISPLAY"); + display.push(displ); + + // Update the count of parameters to display + parametersToDisplay++; + } + + // Store the parameter info + parameterInfo = { + "numParameters": parametersToDisplay, + "numEvents": numEvents, + "names": names, + "compositeNames": compositeNames, + "display": display + } + + // Return it + return parameterInfo; + }, + + callServerSidePluginGenerateFCSPlot: function (model, paramX, paramY, displayX, displayY, numEventsToPlot, totalNumEvents, samplingMethod) { + + // Check whether the data for the plot is already cached + var key = model.dataSetV3.code + "_" + paramX + "_" + paramY + "_" + numEventsToPlot.toString() + + "_" + displayX + "_" + displayY + "_" + samplingMethod.toString(); + + if (model.dataSetV3.code in this.dataCache && + key in this.dataCache[model.dataSetV3.code]) { + + // Plot the cached data + DATAVIEWER.plotFCSData( + this.dataCache[model.dataSetV3.code], + paramX, + paramY, + displayX, + displayY); + + // Return immediately + return; + } + + // Is the reference to the service already stored? + if (null !== this.retrieveFCSEventsService) { + + // Return it + return this.retrieveFCSEventsService; + } + + var thisObj = this; + require(["openbis", + "as/dto/service/search/AggregationServiceSearchCriteria", + "as/dto/service/fetchoptions/AggregationServiceFetchOptions", + "as/dto/service/execute/AggregationServiceExecutionOptions"], + function (openbis, + AggregationServiceSearchCriteria, + AggregationServiceFetchOptions, + AggregationServiceExecutionOptions) { + + // Parameters for the aggregation service + var options = new AggregationServiceExecutionOptions(); + options.withParameter("code", model.dataSetV3.code); + options.withParameter("paramX", paramX); + options.withParameter("paramY", paramY); + options.withParameter("displayX", displayX); + options.withParameter("displayY", displayY); + options.withParameter("numEvents", totalNumEvents); + options.withParameter("maxNumEvents", numEventsToPlot); + options.withParameter("samplingMethod", samplingMethod); + options.withParameter("nodeKey", model.dataSetV3.code); + + // Inform the user that we are about to process the request + thisObj.displayStatus("Please wait while processing your request. This might take a while...", + "info"); + + // Call service + if (null === thisObj.retrieveFCSEventsService) { + var criteria = new AggregationServiceSearchCriteria(); + criteria.withName().thatEquals("retrieve_fcs_events"); + var fetchOptions = new AggregationServiceFetchOptions(); + mainController.openbisV3.searchAggregationServices(criteria, fetchOptions).then(function (result) { + + // Check that we got our service + if (undefined === result.objects) { + console.log("Could not retrieve the server-side aggregation service!"); + return; + } + thisObj.retrieveFCSEventsService = result.getObjects()[0]; + + // Now call the service + mainController.openbisV3.executeAggregationService( + thisObj.retrieveFCSEventsService.getPermId(), + options).then(function (result) { + thisObj.processResultsFromRetrieveFCSEventsServerSidePlugin(result); + }); + }); + } else { + // Call the service + mainController.openbisV3.executeAggregationService( + thisObj.retrieveFCSEventsService.getPermId(), + options).then(function (result) { + thisObj.processResultsFromRetrieveFCSEventsServerSidePlugin(result); + }); + } + }); + }, + + plotFCSData: function (data, xLabel, yLabel, xDisplay, yDisplay) { + + var thisObj = this; + require(["d3", "c3"], function (d3, c3) { + + // Make sure to have a proper array + var parsed_data = JSON.parse(data); + + // Prepend data names to be compatible with C3.js + parsed_data[0].unshift("x_values"); + parsed_data[1].unshift("y_values"); + + // Plot the data + c3.generate({ + bindto: '#detailViewPlot', + title: { + text: yLabel + " vs. " + xLabel + }, + data: { + xs: { + y_values: "x_values" + }, + columns: [ + parsed_data[0], + parsed_data[1], + ], + names: { + y_values: yLabel + }, + type: 'scatter' + }, + axis: { + x: { + label: xLabel, + tick: { + fit: false + } + }, + y: { + label: yLabel, + tick: { + fit: false + } + } + }, + legend: { + show: false + }, + tooltip: { + format: { + title: function (d) { + const format = d3.format(','); + return xLabel + " | " + format(d); + }, + value: function (value, ratio, id) { + const format = d3.format(','); + return format(value); + } + } + }, + zoom: { + enabled: true, + rescale: true + }, + }); + + }); + }, + + processResultsFromRetrieveFCSEventsServerSidePlugin: function (table) { + + // Did we get the expected result? + if (!table.rows || table.rows.length !== 1) { + DATAVIEWER.displayStatus( + "There was an error retrieving the data to plot!", + "danger"); + return; + } + + // Get the row of results + var row = table.rows[0]; + + // Retrieve the uid + var r_UID = row[0].value; + + // Is the process completed? + var r_Completed = row[1].value; + + var thisObj = this; + if (r_Completed === 0) { + + require(["as/dto/service/execute/AggregationServiceExecutionOptions"], + function (AggregationServiceExecutionOptions) { + + // Call the plug-in + setTimeout(function () { + + // Now call the service again: + // we only need the UID of the job + var options = new AggregationServiceExecutionOptions(); + options.withParameter("uid", r_UID); + + mainController.openbisV3.executeAggregationService( + thisObj.retrieveFCSEventsService.getPermId(), + options).then(function (result) { + thisObj.processResultsFromRetrieveFCSEventsServerSidePlugin(result); + }) + }, 2000); + }); + + // Return here + return; + + } + + // We completed the call and we can process the result + + // Returned parameters + var r_Success = row[2].value; + var r_ErrorMessage = row[3].value; + var r_Data = row[4].value; + var r_Code = row[5].value; + var r_ParamX = row[6].value; + var r_ParamY = row[7].value; + var r_DisplayX = row[8].value; + var r_DisplayY = row[9].value; + var r_NumEvents = row[10].value; // Currently not used + var r_MaxNumEvents = row[11].value; + var r_SamplingMethod = row[12].value; + var r_NodeKey = row[13].value; + + var level; + if (r_Success === 1) { + + // Error message and level + status = r_ErrorMessage; + level = "success"; + + // Plot the data + thisObj.plotFCSData(r_Data, r_ParamX, r_ParamY, r_DisplayX, r_DisplayY); + + // Cache the plotted data + var dataKey = r_Code + "_" + r_ParamX + "_" + r_ParamY + "_" + r_MaxNumEvents.toString() + + "_" + r_DisplayX + "_" + r_DisplayY + "_" + r_SamplingMethod.toString(); + thisObj.cacheFCSData(r_NodeKey, dataKey, r_Data); + + } else { + status = "Sorry, there was an error: \"" + r_ErrorMessage + "\"."; + level = "danger"; + } + + // We only display errors + if (r_Success === 0) { + thisObj.displayStatus(status, level); + } else { + thisObj.hideStatus(); + } + + return table; + + }, + + cacheFCSData: function (nodeKey, dataKey, fcsData) { + + // Cache the data + if (! (nodeKey in this.dataCache)) { + this.dataCache[nodeKey] = {}; + } + this.dataCache[nodeKey][dataKey] = fcsData; + }, + + displayStatus: function() { + return; + }, + + hideStatus: function() { + return; } + }); -- GitLab