diff --git a/resources/views/configureQCMetric.html b/resources/views/configureQCMetric.html index 0d65d3017..565e98994 100644 --- a/resources/views/configureQCMetric.html +++ b/resources/views/configureQCMetric.html @@ -230,7 +230,7 @@ const windowConfig = { parent: this, - traces: tracesPresent ? traces : {} , + traces: tracesPresent ? traces : [] , tracesPresent: tracesPresent, operation: op } @@ -239,7 +239,7 @@ windowConfig.metric = clickedMetric; } - Ext4.create('Panorama.Window.AddTraceMetricWindow', windowConfig).show(); + Panorama.Window.AddTraceMetricWindow.show(windowConfig); } }); }, diff --git a/resources/web/PanoramaPremium/window/AddNewTraceMetricWindow.js b/resources/web/PanoramaPremium/window/AddNewTraceMetricWindow.js index 59e10c321..aa46ed4d0 100644 --- a/resources/web/PanoramaPremium/window/AddNewTraceMetricWindow.js +++ b/resources/web/PanoramaPremium/window/AddNewTraceMetricWindow.js @@ -4,518 +4,263 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 */ -Ext4.define('Panorama.Window.AddTraceMetricWindow', { - extend: 'Ext.window.Window', - - modal: true, - closeAction: 'destroy', - bodyStyle: 'padding: 10px;', - autoScroll: true, - border: false, - update: 'update', - insert: 'insert', - timeValueOptions: Ext4.create('Ext.data.Store', { - fields: ['value'], - data : [ - { "value":"First"}, - { "value":"Last"}, - { "value":"Max"}, - { "value":"Min"} - ] - }), - - initComponent: function() { - var title = this.operation === this.insert ? 'Add New Trace Metric' : 'Edit Trace Metric'; - this.setTitle(title); - this.height = Ext4.max([Ext4.getBody().getHeight() * 0.3, 250]); - this.width = Ext4.max([Ext4.getBody().getWidth() * 0.3, 600]); - this.items = this.getItems(); - this.dockedItems= [{ - xtype: 'toolbar', - dock: 'bottom', - ui: 'footer', - items: this.getButtons() - }] - - this.callParent(); - - this.timeValue = true; - this.traceValue = false; - - }, - - getItems: function() { - return [ - this.getMetricNameField(), - this.getTracesCombo(), - this.getYAxisLabelField(), - this.getTraceValueRadioGroup(), - this.getQueryError() - ]; - }, - - getButtons: function () { - var buttons = []; - - buttons.push(this.getCancelButton()); - buttons.push('->'); // to push remaining buttons to the right - if (this.operation === this.update) { - buttons.push(this.getDeleteButton()); - } - buttons.push(this.getSaveButton()); - return buttons; - }, - - getMetricNameField: function() { - if (!this.metricNameField) { - this.metricNameField = Ext4.create('Ext.form.field.Text', { - fieldLabel: 'Metric Name', - labelWidth: 150, - width: 570, - name: 'metricName' - }); +(function($) { + window.Panorama = window.Panorama || {}; + window.Panorama.Window = window.Panorama.Window || {}; - if (this.operation === this.update) { - this.metricNameField.setValue(this.metric.name); - } - } + const DIALOG_ID = 'lk-trace-metric-dialog'; + const TIME_VALUE_OPTIONS = ['First', 'Last', 'Max', 'Min']; + let _config = null; - return this.metricNameField; - }, - - getTracesCombo: function () { - if (!this.tracesCombo) { - var config = { - fieldLabel: 'Use Trace', - name: 'useTrace', - labelWidth: 150, - width: 570 - } - if (!this.tracesPresent) { - config.emptyText = 'No trace can be found'; - } - else { - config.store = this.getTracesComboStore(); - config.valueField = 'TextId'; - config.displayField = 'TextId'; - } - this.tracesCombo = Ext4.create('Ext.form.field.ComboBox', config); - - if (this.operation === this.update) { - this.tracesCombo.setValue(this.metric.TraceName); - this.tracesCombo.bindStore(this.getTracesComboStore()); - } - } - return this.tracesCombo; - }, - - getTracesComboStore: function () { - return Ext4.create('Ext.data.Store', { - fields: ['TextId'], - sorters: [{property: 'TextId'}], - data: this.traces - }); - }, - - getTraceValueRadioGroup: function () { - if (!this.traceValueRadioGroup) { - this.traceValueRadioGroup = - Ext4.create('Ext.form.Panel', { - renderTo: Ext4.getBody(), - width: 570, - - border:false, - items: [{ - xtype: 'radiogroup', - columns: 1, // Stack radio buttons vertically - vertical: true, - items: [ - { - xtype: 'container', - layout: 'hbox', - items: [ - { - xtype: 'radio', - name: 'metricValue', - inputValue: 'timeValue', - boxLabel: 'Use the', - width: 65, - checked: this.operation === this.update ? this.metric.MinTimeValue >= 0 : true, - listeners: { - change: {fn : function(cmp, newVal, oldVal){ - this.minTimeValueNumberField.setDisabled(oldVal); - this.maxTimeValueNumberField.setDisabled(oldVal); - this.timeValueOptionField.setDisabled(oldVal); - this.traceValueNumberField.setDisabled(newVal); - - // restore the value onChange when it is present - if (this.operation === this.update) { - if (newVal) { - this.minTimeValueNumberField.setValue(this.metric.MinTimeValue); - this.maxTimeValueNumberField.setValue(this.metric.MaxTimeValue); - } - else { - this.traceValueNumberField.setValue(this.metric.TraceValue); - } - } - else { - this.traceValueNumberField.setValue(undefined); - this.minTimeValueNumberField.setValue(undefined); - this.maxTimeValueNumberField.setValue(undefined); - this.timeValueOptionField.setValue(undefined); - } - }}, - scope : this - } - }, - this.getTimeValueOptionField(), - { - xtype: 'displayfield', - value: 'trace value when time in minutes is between', - margin: '0 5' - }, - this.getMinTimeValueNumberField(), - { - xtype: 'displayfield', - value: 'and', - margin: '0 5' - }, - this.getMaxTimeValueNumberField() - ] - }, - { - xtype: 'container', - layout: 'hbox', - items: [ - { - xtype: 'radio', - name: 'metricValue', - inputValue: 'traceValue', - boxLabel: 'Use time in minutes when the trace first reaches a value greater than or equal to', - width: 450, - checked: this.operation === this.update ? this.metric.TraceValue > 0 : false - }, - this.getTraceValueNumberField() - ] - } - ] - }] - }); - } - return this.traceValueRadioGroup; - }, - - getTimeValueOptionField: function() { - if (!this.timeValueOptionField) { - this.timeValueOptionField = Ext4.create('Ext.form.field.ComboBox', { - name: 'timeValueOption', - store: this.timeValueOptions, - displayField: 'value', - valueField: 'value', - width: 50, - itemId: 'timeValueOption', - }); - - if(this.operation === this.update) { - this.timeValueOptionField.setValue(this.metric.TimeValueOption); - } - } - - return this.timeValueOptionField - }, - - getMinTimeValueNumberField: function () { - if (!this.minTimeValueNumberField) { - this.minTimeValueNumberField = Ext4.create('Ext.form.field.Number', { - name: 'minTimeValue', - width: 65, - disabled: this.operation === this.update ? !(this.metric.MinTimeValue >= 0) : false - }); - - if (this.operation === this.update) { - this.minTimeValueNumberField.setValue(this.metric.MinTimeValue); - } - - } - return this.minTimeValueNumberField; - }, - - getMaxTimeValueNumberField: function () { - if (!this.maxTimeValueNumberField) { - this.maxTimeValueNumberField = Ext4.create('Ext.form.field.Number', { - name: 'maxTimeValue', - width: 65, - disabled: this.operation === this.update ? !(this.metric.MaxTimeValue >= 0) : false - }); - - if (this.operation === this.update) { - this.maxTimeValueNumberField.setValue(this.metric.MaxTimeValue); - } - - } - return this.maxTimeValueNumberField; - }, - - - getTraceValueNumberField: function () { - if (!this.traceValueNumberField) { - this.traceValueNumberField = Ext4.create('Ext.form.field.Number', { - name: 'traceValue', - width: 65, - disabled: this.operation === this.update ? !(this.metric.TraceValue >= 0) : true - }); + function closeDialog() { + $('#' + DIALOG_ID).remove(); + } - if (this.operation === this.update) { - this.traceValueNumberField.setValue(this.metric.TraceValue); - } - } - return this.traceValueNumberField; - }, - - getYAxisLabelField: function() { - if (!this.yAxisLabelField) { - this.yAxisLabelField = Ext4.create('Ext.form.field.Text', { - fieldLabel: 'Y Axis Label', - labelWidth: 150, - width: 570, - name: 'yAxisLabel' - }); + function showError(msg) { + $('#lk-trace-metric-error').text(msg).show(); + } - if(this.operation === this.update) { - this.yAxisLabelField.setValue(this.metric.YAxisLabel); - } - } + function clearErrors() { + $('#lk-trace-metric-error').hide().text(''); + $('#lk-trace-metric-name, #lk-trace-use-trace, #lk-trace-ylabel, #lk-trace-time-option, #lk-trace-min-time, #lk-trace-max-time, #lk-trace-value') + .css('border-color', ''); + } - return this.yAxisLabelField; - }, + function markInvalid($field) { + $field.css('border-color', 'red'); + } - getQueryError: function() { - if (!this.queryError) { - this.queryError = Ext4.create('Ext.form.Label', { - name: 'errorMsg', - hidden: true, - cls: 'labkey-error', - text:'' - }); - } + function getMode() { + return $('input[name="metricValue"]:checked').val(); // 'timeValue' or 'traceValue' + } - return this.queryError; - }, + // Enable only the fields belonging to the selected mode + function refreshMode() { + const isTime = getMode() === 'timeValue'; + $('#lk-trace-time-option, #lk-trace-min-time, #lk-trace-max-time').prop('disabled', !isTime); + $('#lk-trace-value').prop('disabled', isTime); + } - getSaveButton: function() { - if (!this.saveButton) { - this.saveButton = Ext4.create('Ext.button.Button', { - text: 'Save', - scope: this, - handler: this.saveNewMetric, - disabled: !this.tracesPresent - }); - } - return this.saveButton; - }, - - getDeleteButton: function() { - if (!this.deleteButton) { - this.deleteButton = Ext4.create('Ext.button.Button', { - text: 'Delete', - scope: this, - handler: this.deleteMetric - }); - } - return this.deleteButton; - }, - - getCancelButton: function() { - if (!this.cancelButton) { - this.cancelButton = Ext4.create('Ext.button.Button', { - text: 'Cancel', - scope: this, - handler: function(btn){ - btn.up('window').close(); - } - }); - } - return this.cancelButton; - }, + function isNonNegativeNumber(val) { + return val !== '' && val !== null && !isNaN(val) && Number(val) >= 0; + } - validateValues: function() { - var isValid = true; - var errorText = 'Required'; + function validate() { + clearErrors(); + let isValid = true; - if (!(this.metricNameField.getValue().length > 0)) { - this.metricNameField.setActiveError(errorText); + if (!$('#lk-trace-metric-name').val().trim()) { + markInvalid($('#lk-trace-metric-name')); isValid = false; } - - if (!this.tracesCombo.getValue()) { - this.tracesCombo.setActiveError(errorText); + if (!$('#lk-trace-use-trace').val()) { + markInvalid($('#lk-trace-use-trace')); isValid = false; } - - if (!(this.yAxisLabelField.getValue().length > 0)) { - this.yAxisLabelField.setActiveError(errorText); + if (!$('#lk-trace-ylabel').val().trim()) { + markInvalid($('#lk-trace-ylabel')); isValid = false; } - if (this.timeValueOptionField.getValue() === null) { - this.timeValueOptionField.setActiveError(errorText); - isValid = false; - } - - if (this.traceValueRadioGroup.down().getValue()['metricValue'] === 'timeValue' && - (!(this.minTimeValueNumberField.getValue() >= 0))) { - this.minTimeValueNumberField.setActiveError(errorText); - isValid = false; + if (getMode() === 'timeValue') { + if (!$('#lk-trace-time-option').val()) { + markInvalid($('#lk-trace-time-option')); + isValid = false; + } + if (!isNonNegativeNumber($('#lk-trace-min-time').val())) { + markInvalid($('#lk-trace-min-time')); + isValid = false; + } + if (!isNonNegativeNumber($('#lk-trace-max-time').val())) { + markInvalid($('#lk-trace-max-time')); + isValid = false; + } } - - if (this.traceValueRadioGroup.down().getValue()['metricValue'] === 'timeValue' && - (!(this.maxTimeValueNumberField.getValue() >= 0))) { - this.maxTimeValueNumberField.setActiveError(errorText); + else if (!isNonNegativeNumber($('#lk-trace-value').val())) { + markInvalid($('#lk-trace-value')); isValid = false; } - if (this.traceValueRadioGroup.down().getValue()['metricValue'] === 'traceValue' && !this.traceValueNumberField.getValue()) { - this.traceValueNumberField.setActiveError(errorText); - isValid = false; + if (!isValid) { + showError('Please fill in all required fields.'); } - return isValid; - }, - - checkMetricNameExists: function (metricName, callback) { - let filterArray = [LABKEY.Filter.create('Name', metricName, LABKEY.Filter.Types.EQUAL)]; + } - // If updating, exclude the current metric from the check - if (this.operation === this.update && this.metric) { - filterArray.push(LABKEY.Filter.create('id', this.metric.id, LABKEY.Filter.Types.NOT_EQUAL)); + function checkMetricNameExists(metricName, callback) { + const filterArray = [LABKEY.Filter.create('Name', metricName, LABKEY.Filter.Types.EQUAL)]; + if (_config.operation === 'update' && _config.metric) { + filterArray.push(LABKEY.Filter.create('id', _config.metric.id, LABKEY.Filter.Types.NOT_EQUAL)); } - LABKEY.Query.selectRows({ containerPath: LABKEY.container.id, schemaName: 'targetedms', queryName: 'qcmetricconfiguration', filterArray: filterArray, - scope: this, - success: function (data) { - callback.call(this, data.rows.length > 0); - }, - failure: function () { - callback.call(this, false); - } + success: function(data) { callback(data.rows.length > 0); }, + failure: function() { callback(false); } }); - }, - - - - saveNewMetric: function () { - var isValid = this.validateValues(); - - if (isValid) { - var metricName = this.metricNameField.getValue(); - - this.checkMetricNameExists(metricName, function (exists) { - if (exists) { - let errorMessage = 'A metric with the name "' + metricName + '" already exists. Please choose a different name.'; - this.queryError.setText(errorMessage); - this.queryError.setVisible(true); - this.metricNameField.setActiveError('Metric name already exists'); - return; - } + } - var records = []; - var newMetric = {}; - newMetric.Name = metricName; - newMetric.QueryName = 'QCTraceMetric'; // dummy text to insert and not an actual query - newMetric.PrecursorScoped = false; - newMetric.TraceName = this.tracesCombo.getValue(); - newMetric.YAxisLabel = this.yAxisLabelField.getValue(); - - if (this.traceValueNumberField.getValue()) { - newMetric.TraceValue = this.traceValueNumberField.getValue(); - } else { - if (this.timeValueOptionField.getValue()) { - newMetric.TimeValueOption = this.timeValueOptionField.getValue(); - } - if (this.minTimeValueNumberField.getValue()) { - newMetric.MinTimeValue = this.minTimeValueNumberField.getValue(); - } - if (this.maxTimeValueNumberField.getValue()) { - newMetric.MaxTimeValue = this.maxTimeValueNumberField.getValue(); - } - } + function save() { + if (!validate()) return; - if (this.operation === this.update) { - newMetric.id = this.metric.id; - } + const metricName = $('#lk-trace-metric-name').val().trim(); + checkMetricNameExists(metricName, function(exists) { + if (exists) { + showError('A metric with the name "' + LABKEY.Utils.encodeHtml(metricName) + '" already exists. Please choose a different name.'); + markInvalid($('#lk-trace-metric-name')); + return; + } + + const newMetric = { + Name: metricName, + QueryName: 'QCTraceMetric', // dummy text to insert and not an actual query + PrecursorScoped: false, + TraceName: $('#lk-trace-use-trace').val(), + YAxisLabel: $('#lk-trace-ylabel').val().trim() + }; + + if (getMode() === 'traceValue') { + newMetric.TraceValue = Number($('#lk-trace-value').val()); + } + else { + newMetric.TimeValueOption = $('#lk-trace-time-option').val(); + newMetric.MinTimeValue = Number($('#lk-trace-min-time').val()); + newMetric.MaxTimeValue = Number($('#lk-trace-max-time').val()); + } + + if (_config.operation === 'update') { + newMetric.id = _config.metric.id; + } - records.push(newMetric); - - LABKEY.Query.saveRows({ - containerPath: LABKEY.container.id, - commands: [{ - schemaName: 'targetedms', - queryName: 'qcmetricconfiguration', - command: this.operation, - rows: records - }], - scope: this, - method: 'POST', - success: function () { - window.location.reload(); - }, - failure: function (response) { - let errorMessage = 'Error saving metric'; - if (response && response.exception) { - errorMessage = response.exception; - } else if (response && response.message) { - errorMessage = response.message; - } - this.queryError.setText(errorMessage); - this.queryError.setVisible(true); - } - }); + LABKEY.Query.saveRows({ + containerPath: LABKEY.container.id, + commands: [{ schemaName: 'targetedms', queryName: 'qcmetricconfiguration', command: _config.operation, rows: [newMetric] }], + method: 'POST', + success: function() { window.location.reload(); }, + failure: function(response) { + showError((response && (response.exception || response.message)) || 'Error saving metric'); + } }); - } + }); + } + + function deleteMetric() { + if (!confirm('This will delete the "' + _config.metric.name + '" metric. Are you sure?')) return; - }, - - deleteMetric: function() { - Ext4.Msg.confirm('Delete Trace Metric', 'This will delete ' + LABKEY.Utils.encodeHtml(this.metric.name) + ' metric. Are you sure you want to do this?', function(val){ - if (val === 'yes'){ - const qcMetricToDelete = {metric: this.metric.id}; - const metricToDelete = {id: this.metric.id}; - - LABKEY.Query.saveRows({ - containerPath: LABKEY.container.id, - commands: [{ - schemaName: 'targetedms', - queryName: 'qcenabledmetrics', - command: 'delete', - rows: [qcMetricToDelete] - },{ - schemaName: 'targetedms', - queryName: 'qcmetricconfiguration', - command: 'delete', - rows: [metricToDelete] - }], - scope: this, - method: 'POST', - success: function () { - window.location.reload(); - }, - failure: function (response) { - let errorMessage = 'Error saving metric'; - if (response && response.exception) { - errorMessage = response.exception; - } else if (response && response.message) { - errorMessage = response.message; - } - this.queryError.setText(errorMessage); - this.queryError.setVisible(true); - } - }); - win.close(); + LABKEY.Query.saveRows({ + containerPath: LABKEY.container.id, + commands: [ + { schemaName: 'targetedms', queryName: 'qcenabledmetrics', command: 'delete', rows: [{ metric: _config.metric.id }] }, + { schemaName: 'targetedms', queryName: 'qcmetricconfiguration', command: 'delete', rows: [{ id: _config.metric.id }] } + ], + method: 'POST', + success: function() { window.location.reload(); }, + failure: function(response) { + showError((response && (response.exception || response.message)) || 'Error deleting metric'); } - }, this); + }); + } + + function buildTraceOptions(selectedTrace) { + const traces = (_config.traces || []).slice().sort(function(a, b) { + return String(a.TextId).localeCompare(String(b.TextId)); + }); + let html = ''; + traces.forEach(function(row) { + const textId = row.TextId; + const sel = textId === selectedTrace ? ' selected' : ''; + html += ''; + }); + return html; + } + + function buildTimeOptions(selectedOption) { + let html = ''; + TIME_VALUE_OPTIONS.forEach(function(opt) { + const sel = opt === selectedOption ? ' selected' : ''; + html += ''; + }); + return html; + } + + function buildDialogHtml() { + const op = _config.operation; + const metric = _config.metric || {}; + const tracesPresent = !!_config.tracesPresent; + const title = op === 'insert' ? 'Add New Trace Metric' : 'Edit Trace Metric'; + + // In update mode, pick the mode based on the stored values; default to the time-value mode. + const isTraceValueMode = op === 'update' && metric.TraceValue > 0; + + const num = function(v) { + return (v !== undefined && v !== null) ? LABKEY.Utils.encodeHtml(v) : ''; + }; + + const traceSelect = tracesPresent + ? '' + : ''; + + return '
' + + '
' + + '
' + + '

' + LABKEY.Utils.encodeHtml(title) + '

' + + '
' + + '
' + + '' + + '' + + '' + + '' + + '' + + '' + + '' + + '
' + traceSelect + '
' + + '
' + + '
' + + '' + + '' + + 'trace value when time in minutes is between' + + '' + + 'and' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '' + + '
' + + '' + + (op === 'update' ? ' ' : '') + + ' ' + + '
' + + '
' + + '
' + + '
'; } -}); + + window.Panorama.Window.AddTraceMetricWindow = { + show: function(config) { + _config = config; + + $('#' + DIALOG_ID).remove(); + $('body').append(buildDialogHtml()); + + $('#lk-trace-metric-cancel').on('click', closeDialog); + $('#lk-trace-metric-save').on('click', save); + if (config.operation === 'update') { + $('#lk-trace-metric-delete').on('click', deleteMetric); + } + $('input[name="metricValue"]').on('change', refreshMode); + + // close on overlay click + $('#' + DIALOG_ID).on('click', function(e) { + if (e.target === this) closeDialog(); + }); + + refreshMode(); + } + }; +})(jQuery); diff --git a/src/org/labkey/targetedms/view/calibrationCurve.jsp b/src/org/labkey/targetedms/view/calibrationCurve.jsp index 1fefd7f5e..18d011ccb 100644 --- a/src/org/labkey/targetedms/view/calibrationCurve.jsp +++ b/src/org/labkey/targetedms/view/calibrationCurve.jsp @@ -30,7 +30,7 @@ @Override public void addClientDependencies(ClientDependencies dependencies) { - dependencies.add("Ext4"); + dependencies.add("Ext4"); // LABKEY.vis (vis/vis) still uses Ext.isArray for log-scale axes dependencies.add("vis/vis"); dependencies.add("targetedms/js/CalibrationCurve.js"); dependencies.add("targetedms/css/CalibrationCurve.css"); @@ -46,9 +46,9 @@ var calibrationCurvePlot; - Ext4.onReady(function () { + LABKEY.Utils.onReady(function () { - calibrationCurvePlot = Ext4.create('LABKEY.targetedms.CalibrationCurve', { + calibrationCurvePlot = new LABKEY.targetedms.CalibrationCurve({ renderTo: <%=q(elementId)%>, data: <%=bean%> }); diff --git a/src/org/labkey/targetedms/view/paretoPlot.jsp b/src/org/labkey/targetedms/view/paretoPlot.jsp index 6282a42fe..56ab6ad5b 100644 --- a/src/org/labkey/targetedms/view/paretoPlot.jsp +++ b/src/org/labkey/targetedms/view/paretoPlot.jsp @@ -27,11 +27,10 @@ @Override public void addClientDependencies(ClientDependencies dependencies) { - dependencies.add("Ext4"); + dependencies.add("Ext4"); // still needed by QCMetricConfigLoader.js dependencies.add("vis/vis"); dependencies.add("targetedms/css/SVGExportIcon.css"); dependencies.add("targetedms/css/ParetoPlot.css"); - dependencies.add("targetedms/js/BaseQCPlotPanel.js"); dependencies.add("targetedms/js/ParetoPlotPanel.js"); dependencies.add("targetedms/js/QCMetricConfigLoader.js"); } @@ -47,22 +46,11 @@ function init() { var tiledPlotPanelId = <%=q(tiledPlotPanelId)%>; - if (Ext4.isIE8) { - Ext4.get(tiledPlotPanelId).update("Unable to render report in Internet Explorer < 9."); - return; - } - - initializeParetoPlotPanel(tiledPlotPanelId); - } - - function initializeParetoPlotPanel(tiledPlotPanelId) { - - // initialize the panel that displays Pareto plot - Ext4.create('LABKEY.targetedms.ParetoPlotPanel', { - cls: 'themed-panel', + // initialize the panel that displays Pareto plots + new LABKEY.targetedms.ParetoPlotPanel({ plotDivId: tiledPlotPanelId }); } - Ext4.onReady(init); + LABKEY.Utils.onReady(init); diff --git a/src/org/labkey/targetedms/view/summaryChartsView.jsp b/src/org/labkey/targetedms/view/summaryChartsView.jsp index 2156b8af7..965e37a89 100644 --- a/src/org/labkey/targetedms/view/summaryChartsView.jsp +++ b/src/org/labkey/targetedms/view/summaryChartsView.jsp @@ -30,7 +30,6 @@ @Override public void addClientDependencies(ClientDependencies dependencies) { - dependencies.add("Ext4"); dependencies.add("internal/jQuery"); dependencies.add("TargetedMS/js/svgChart.js"); dependencies.add("TargetedMS/css/svgChart.css"); @@ -62,7 +61,9 @@ long moleculeId = bean.getMoleculeId(); long moleculePrecursorId = bean.getMoleculePrecursorId(); - if ((peptideList != null && !peptideList.isEmpty()) || peptideId != 0 || precursorId != 0) + boolean asProteomics = (peptideList != null && !peptideList.isEmpty()) || peptideId != 0 || precursorId != 0; + + if (asProteomics) { peakAreaUrl.addParameter("asProteomics", true); retentionTimesUrl.addParameter("asProteomics", true); @@ -98,6 +99,17 @@ peakAreaUrl.addParameter("chartHeight", bean.getInitialHeight()); retentionTimesUrl.addParameter("chartWidth", bean.getInitialWidth()); retentionTimesUrl.addParameter("chartHeight", bean.getInitialHeight()); + + // The ExtJS stores prepended "All"/"None" entries, so the original visibility checks were + // count-based (e.g. store.count() > 2). Translate those to list-size checks here. + boolean hasPeptides = peptideList != null && !peptideList.isEmpty(); + boolean hasMolecules = moleculeList != null && !moleculeList.isEmpty(); + boolean showReplicate = replicateList.size() > 1; + boolean showAnnotName = !replicateAnnotationNameList.isEmpty(); + boolean showAnnotValue = !replicateAnnotationValueList.isEmpty(); + boolean showPeptide = hasPeptides && peptideList.size() > 1; + boolean showMolecule = hasMolecules && moleculeList.size() > 1; + boolean showCv = showReplicate || showAnnotName; %> +
+
;"> + + ;"> + + + + ;"> + + + + ;"> + + + + ;"> + + + + ;"> + + + + ;"> + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ +
+ +
Value only effects Retention Times chart.
+
+
+

Peak Areas

+
+
+
+
+

Retention Times

+
+
+
+
-
-
;">
-
-

Peak Areas

-
-
-
-
-

Retention Times

-
-
-
-
- diff --git a/webapp/TargetedMS/js/CalibrationCurve.js b/webapp/TargetedMS/js/CalibrationCurve.js index 20fb85ef4..479ae906e 100644 --- a/webapp/TargetedMS/js/CalibrationCurve.js +++ b/webapp/TargetedMS/js/CalibrationCurve.js @@ -5,27 +5,29 @@ */ /** * Created by Marty on 3/16/2017. + * + * Plain JS/HTML implementation (no ExtJS). Renders the calibration curve plot (LABKEY.vis) + * into the element identified by config.renderTo. */ - -Ext4.define('LABKEY.targetedms.CalibrationCurve', { - - extend: 'Ext.panel.Panel', - layout: 'fit', - border: false, - - selectedPointLayer: null, - plotHeight: 500, - minWidth: 800, - - colors: { - unknown: 'black', - standard: 'gray', - qc: 'green' - }, - - initComponent: function () { - Ext4.tip.QuickTipManager.init(); - this.callParent(); +if (!LABKEY.targetedms) { + LABKEY.targetedms = {}; +} + +(function() { + + function CalibrationCurve(config) { + this.renderTo = config.renderTo; + this.data = config.data; + + this.selectedPointLayer = null; + this.plotHeight = 500; + this.minWidth = 800; + this.plot = null; + this.colors = { + unknown: 'black', + standard: 'gray', + qc: 'green' + }; this.width = this.getPanelSize(); @@ -35,281 +37,288 @@ Ext4.define('LABKEY.targetedms.CalibrationCurve', { this.maxX = this.data.calibrationCurve.maxX || 0; // Ensure plot goes to max x axis for selected point calculations - var calcMaxX = this.getQuadraticIntersect(this, this.maxY); - if (calcMaxX > this.maxX) + const calcMaxX = this.getQuadraticIntersect(this, this.maxY); + if (calcMaxX > this.maxX) { this.maxX = calcMaxX; + } this.addCurvePoints(); this.refreshPlot(); - var me = this; - window.addEventListener("resize", function () { + const me = this; + window.addEventListener("resize", function() { // Issue 43532 - may have been loaded even if we don't have a curve to plot if (me.plot) { - me.setWidth(me.getPanelSize()); - me.plot.setWidth(me.getWidth()); + me.width = me.getPanelSize(); + me.plot.setWidth(me.width); me.plot.render(); // Plot re-renders so need to shrink dots to get back to initial state d3.selectAll('a.point path').transition().attr("stroke-width", 1); } }, false); - }, - - refreshPlot: function() { - // Clear out child elements - let children = document.getElementById(this.renderTo).childNodes; - for (let i = 0; i < children.length; ) { - if (children[i].localName === 'svg') { - children[i].parentNode.removeChild(children[i]); + } + + CalibrationCurve.prototype = { + + refreshPlot: function() { + // Clear out child elements + const children = document.getElementById(this.renderTo).childNodes; + for (let i = 0; i < children.length; ) { + if (children[i].localName === 'svg') { + children[i].parentNode.removeChild(children[i]); + } + else { + i++; + } } - else { - i++; + this.addPlot(); + }, + + // Add points for quadratic calculated concentration curve + addCurvePoints: function() { + const curvePts = 50; + let x, y; + const increment = (this.maxX - this.minX) / curvePts; + + this.data.curvePoints = []; + for (let pt = 0; pt <= curvePts; pt++) { + x = this.minX + (pt * increment); + y = this.data.calibrationCurve.quadraticCoefficient * (x * x) + this.data.calibrationCurve.slope * x + + this.data.calibrationCurve.intercept; + + this.data.curvePoints.push({x: x, y: y}); } - } - this.addPlot(); - }, - - // Add points for quadratic calculated concentration curve - addCurvePoints: function () { - var curvePts = 50; - var x, y; - var increment = (this.maxX - this.minX) / curvePts; - - this.data.curvePoints = []; - for (var pt = 0; pt <= curvePts; pt++) { - x = this.minX + (pt * increment); - y = this.data.calibrationCurve.quadraticCoefficient * (x * x) + this.data.calibrationCurve.slope * x - + this.data.calibrationCurve.intercept; - - this.data.curvePoints.push({x:x, y:y}); - } - }, + }, - getPanelSize: function () { - return window.innerWidth - 100; - }, + getPanelSize: function() { + return window.innerWidth - 100; + }, - // Given y, solve for x - getQuadraticIntersect: function (scope, y) { - var a = scope.data.calibrationCurve.quadraticCoefficient; - var b = scope.data.calibrationCurve.slope; - var c = scope.data.calibrationCurve.intercept; + // Given y, solve for x + getQuadraticIntersect: function(scope, y) { + const a = scope.data.calibrationCurve.quadraticCoefficient; + const b = scope.data.calibrationCurve.slope; + const c = scope.data.calibrationCurve.intercept; - var intersect; - if (a !== 0) { //Quadratic - intersect = ((-1 * b) + Math.sqrt((b * b) - (4 * a * (c - y)))) / (2 * a); - } - else { //Linear - intersect = (y - c) / b; - } - return intersect; - }, - - getPointToLineLayer: function (scope, point) { - var data = []; - data.push(point); - data.push({ - x: scope.getQuadraticIntersect(scope, point.y), - y: point.y, - type: point.type - }); - - data.push({ - x: scope.getQuadraticIntersect(scope, point.y), - y: scope.minY, - type: point.type - }); - - return new LABKEY.vis.Layer({ - geom: new LABKEY.vis.Geom.Path({size: 3, opacity: 0, color: 'red'}), - aes: { - y: function (row) { - return row.y + let intersect; + if (a !== 0) { //Quadratic + intersect = ((-1 * b) + Math.sqrt((b * b) - (4 * a * (c - y)))) / (2 * a); + } + else { //Linear + intersect = (y - c) / b; + } + return intersect; + }, + + getPointToLineLayer: function(scope, point) { + const data = []; + data.push(point); + data.push({ + x: scope.getQuadraticIntersect(scope, point.y), + y: point.y, + type: point.type + }); + + data.push({ + x: scope.getQuadraticIntersect(scope, point.y), + y: scope.minY, + type: point.type + }); + + return new LABKEY.vis.Layer({ + geom: new LABKEY.vis.Geom.Path({size: 3, opacity: 0, color: 'red'}), + aes: { + y: function(row) { + return row.y + }, + x: function(row) { + return row.x + } }, - x: function (row) { - return row.x - } - }, - data: data - }) - }, + data: data + }) + }, - addPlot: function () { - var me = this; + addPlot: function() { + const me = this; - if (this.data.calibrationCurve.errorMessage) { - document.getElementById(this.renderTo).innerText = this.data.calibrationCurve.errorMessage; - return; - } + if (this.data.calibrationCurve.errorMessage) { + document.getElementById(this.renderTo).innerText = this.data.calibrationCurve.errorMessage; + return; + } - // This is a dummy layer to be overwritten by the line layer when selecting a point - this.selectedPointLayer = new LABKEY.vis.Layer({ - geom: new LABKEY.vis.Geom.Path({size: 3, opacity: 0}), - data: [], - aes: { - y: function (row) { - return row.y; - }, - x: function (row) { - return row.x; + // This is a dummy layer to be overwritten by the line layer when selecting a point + this.selectedPointLayer = new LABKEY.vis.Layer({ + geom: new LABKEY.vis.Geom.Path({size: 3, opacity: 0}), + data: [], + aes: { + y: function(row) { + return row.y; + }, + x: function(row) { + return row.x; + } } + }); + + let units = ""; + if (this.data.calibrationCurve.units != null) { + units = "(" + this.data.calibrationCurve.units + ")"; } - }); - - var units = ""; - if (this.data.calibrationCurve.units != null) - units = "(" + this.data.calibrationCurve.units + ")"; - - this.plot = new LABKEY.vis.Plot({ - renderTo: this.renderTo, - rendererType: 'd3', - width: this.width, - height: this.plotHeight, - labels: { - main: {value: this.data.molecule.name}, - y: {value: 'Normalized Peak Areas'}, - x: {value: 'Analyte Concentration ' + units} - }, - layers: [ - this.selectedPointLayer, - new LABKEY.vis.Layer({ - data: this.data.curvePoints, - geom: new LABKEY.vis.Geom.Path({size: 3, opacity: .4}), - aes: { - y: 'y', - x: 'x' - } - }), - new LABKEY.vis.Layer({ - data: this.data.dataPoints, - geom: new LABKEY.vis.Geom.Point({size: 5, opacity: 0.75}), - aes: { - y: 'y', - x: 'x', - pointClickFn: function (event, data) { - var legend = me.getLegendDataInfo(me) - .concat(me.getLegendDataSlopeCalculations(me)) - .concat(me.getLegendDataPointCalculations(me, data)); - - me.plot.setLegend(legend); - - var lineLayer = me.getPointToLineLayer(me, data); - - me.plot.replaceLayer(me.selectedPointLayer, lineLayer); - me.selectedPointLayer = lineLayer; - me.plot.render(); - - // Shrink dots from previous clicks and grow clicked dot - d3.selectAll('a.point path').transition().attr("stroke-width", 1); - d3.select(event.srcElement).transition().attr("stroke-width", 8); - - // Transition in line layer visibility - d3.selectAll('svg g.layer path[stroke-opacity="0"').transition().attr('stroke-opacity', .5) - }, - hoverText: function (row) { - return 'Name: ' + row.name + '\nPeak Area: ' + me.formatLegendValue(row.y, true) + '\nConcentration: ' + me.formatLegendValue(row.x) + (row.excluded ? '\nExcluded from calibration' : ''); + + this.plot = new LABKEY.vis.Plot({ + renderTo: this.renderTo, + rendererType: 'd3', + width: this.width, + height: this.plotHeight, + labels: { + main: {value: this.data.molecule.name}, + y: {value: 'Normalized Peak Areas'}, + x: {value: 'Analyte Concentration ' + units} + }, + layers: [ + this.selectedPointLayer, + new LABKEY.vis.Layer({ + data: this.data.curvePoints, + geom: new LABKEY.vis.Geom.Path({size: 3, opacity: .4}), + aes: { + y: 'y', + x: 'x' + } + }), + new LABKEY.vis.Layer({ + data: this.data.dataPoints, + geom: new LABKEY.vis.Geom.Point({size: 5, opacity: 0.75}), + aes: { + y: 'y', + x: 'x', + pointClickFn: function(event, data) { + const legend = me.getLegendDataInfo(me) + .concat(me.getLegendDataSlopeCalculations(me)) + .concat(me.getLegendDataPointCalculations(me, data)); + + me.plot.setLegend(legend); + + const lineLayer = me.getPointToLineLayer(me, data); + + me.plot.replaceLayer(me.selectedPointLayer, lineLayer); + me.selectedPointLayer = lineLayer; + me.plot.render(); + + // Shrink dots from previous clicks and grow clicked dot + d3.selectAll('a.point path').transition().attr("stroke-width", 1); + d3.select(event.srcElement).transition().attr("stroke-width", 8); + + // Transition in line layer visibility + d3.selectAll('svg g.layer path[stroke-opacity="0"').transition().attr('stroke-opacity', .5) + }, + hoverText: function(row) { + return 'Name: ' + row.name + '\nPeak Area: ' + me.formatLegendValue(row.y, true) + '\nConcentration: ' + me.formatLegendValue(row.x) + (row.excluded ? '\nExcluded from calibration' : ''); + } } + }) + ], + aes: { + color: function(row) { + return row.type; + }, + shape: function(row) { + return row.excluded ? 'Excluded' : 'Included'; } - }) - ], - aes: { - color: function (row) { - return row.type; }, - shape: function (row) { - return row.excluded ? 'Excluded' : 'Included'; - } - }, - scales: { - color: { - scaleType: 'discrete', - scale: function (group) { - if (Ext4.isDefined(me.colors[group])) - return me.colors[group]; - - return 'blue'; + scales: { + color: { + scaleType: 'discrete', + scale: function(group) { + if (me.colors[group] !== undefined) + return me.colors[group]; + + return 'blue'; + }, }, - }, - y: { - scaleType: 'continuous', - trans: document.getElementById('calCurveYScale').value, - domain: [me.minY, me.maxY], - tickFormat: function (d) { - if (d < 1000 && d > 0.001) - return d; - return d.toExponential(); + y: { + scaleType: 'continuous', + trans: document.getElementById('calCurveYScale').value, + domain: [me.minY, me.maxY], + tickFormat: function(d) { + if (d < 1000 && d > 0.001) + return d; + return d.toExponential(); + } + }, + x: { + trans: document.getElementById('calCurveXScale').value } }, - x: { - trans: document.getElementById('calCurveXScale').value + legendData: this.getLegendDataInfo(me).concat(this.getLegendDataSlopeCalculations(me)), + legendNoWrap: true + }); + + this.plot.render(); + + LABKEY.targetedms.SVGChart.attachPlotExportIcons(this.renderTo, 'Calibration Curve: ' + this.data.molecule.name, 800, 0); + }, + + getLegendDataPointCalculations: function(scope, point) { + + const result = [ + {text: 'Selected Point', separator: true}, + {text: 'Replicate: ' + point.name, color: 'white'}, + {text: 'Peak Area: ' + scope.formatLegendValue(point.y, true), color: 'white'}, + {text: 'Concentration: ' + scope.formatLegendValue(point.x), color: 'white'}, + { + text: 'Calc. Concentration: ' + scope.formatLegendValue(scope.getQuadraticIntersect(scope, point.y)), + color: 'white' } - }, - legendData: this.getLegendDataInfo(me).concat(this.getLegendDataSlopeCalculations(me)), - legendNoWrap: true - }); - - this.plot.render(); - - LABKEY.targetedms.SVGChart.attachPlotExportIcons(this.renderTo, 'Calibration Curve: ' + this.data.molecule.name, 800, 0); - }, - - getLegendDataPointCalculations: function (scope, point) { - - var result = [ - {text: 'Selected Point', separator: true}, - {text: 'Replicate: ' + point.name, color: 'white'}, - {text: 'Peak Area: ' + scope.formatLegendValue(point.y, true), color: 'white'}, - {text: 'Concentration: ' + scope.formatLegendValue(point.x), color: 'white'}, - { - text: 'Calc. Concentration: ' + scope.formatLegendValue(scope.getQuadraticIntersect(scope, point.y)), - color: 'white' + ]; + if (point.excluded) { + result.push({text: 'Excluded from calibration', color: 'white'}); } - ]; - if (point.excluded) { - result.push({text: 'Excluded from calibration', color: 'white'}); - } - return result - }, - - getLegendDataSlopeCalculations: function (scope) { - var result = [ - {text: 'Calibration Curve', separator: true}, - {text: 'Regression Fit: ' + Ext4.util.Format.htmlEncode(this.data.calibrationCurve.regressionFit), color: 'white'}, - {text: 'Norm. Method: ' + Ext4.util.Format.htmlEncode(this.data.calibrationCurve.normalizationMethod), color: 'white'}, - {text: 'Regression Weighting: ' + Ext4.util.Format.htmlEncode(this.data.calibrationCurve.regressionWeighting), color: 'white'}, - {text: 'MS Level: ' + (this.data.msLevel > 0 ? this.data.msLevel : 'All'), color: 'white'}, - {text: '', separator: true}, - {text: 'Slope: ' + scope.formatLegendValue(this.data.calibrationCurve.slope, true), color: 'white'}, - {text: 'Intercept: ' + scope.formatLegendValue(this.data.calibrationCurve.intercept, true), color: 'white'} - ]; - if (this.data.calibrationCurve.quadraticCoefficient && this.data.calibrationCurve.quadraticCoefficient !== 0.0) { - result.push({text: 'Quadratic Coefficient: ' + scope.formatLegendValue(this.data.calibrationCurve.quadraticCoefficient), color: 'white'}); + return result + }, + + getLegendDataSlopeCalculations: function(scope) { + const result = [ + {text: 'Calibration Curve', separator: true}, + {text: 'Regression Fit: ' + LABKEY.Utils.encodeHtml(this.data.calibrationCurve.regressionFit), color: 'white'}, + {text: 'Norm. Method: ' + LABKEY.Utils.encodeHtml(this.data.calibrationCurve.normalizationMethod), color: 'white'}, + {text: 'Regression Weighting: ' + LABKEY.Utils.encodeHtml(this.data.calibrationCurve.regressionWeighting), color: 'white'}, + {text: 'MS Level: ' + (this.data.msLevel > 0 ? this.data.msLevel : 'All'), color: 'white'}, + {text: '', separator: true}, + {text: 'Slope: ' + scope.formatLegendValue(this.data.calibrationCurve.slope, true), color: 'white'}, + {text: 'Intercept: ' + scope.formatLegendValue(this.data.calibrationCurve.intercept, true), color: 'white'} + ]; + if (this.data.calibrationCurve.quadraticCoefficient && this.data.calibrationCurve.quadraticCoefficient !== 0.0) { + result.push({text: 'Quadratic Coefficient: ' + scope.formatLegendValue(this.data.calibrationCurve.quadraticCoefficient), color: 'white'}); + } + result.push({text: 'rSquared: ' + scope.formatLegendValue(this.data.calibrationCurve.rSquared), color: 'white'}); + result.push({text: '', separator: true}); + return result; + }, + + getLegendDataInfo: function(scope) { + return [ + {text: 'Standard', color: scope.colors['standard'], shape: LABKEY.vis.Scale.Shape()[0]}, + {text: 'QC', color: scope.colors['qc'], shape: LABKEY.vis.Scale.Shape()[0]}, + {text: 'Unknown', color: scope.colors['unknown'], shape: LABKEY.vis.Scale.Shape()[0]}, + {text: '', separator: true}, + {text: 'Excluded', color: scope.colors['standard'], shape: LABKEY.vis.Scale.Shape()[1]}, + {text: '', separator: true} + ]; + }, + + formatLegendValue: function(value, exp) { + if (value == null) + return 'NaN'; + const rounded = Math.round(value * 100000) / 100000; + // Use scientific notation if the number is large or very small + if (exp && (rounded > 10000 || rounded < -10000 || (rounded > -0.00001 && rounded < 0.00001))) + rounded.toExponential(4) + return rounded; } - result.push({text: 'rSquared: ' + scope.formatLegendValue(this.data.calibrationCurve.rSquared), color: 'white'}); - result.push({text: '', separator: true}); - return result; - }, - - getLegendDataInfo: function (scope) { - return [ - {text: 'Standard', color: scope.colors['standard'], shape:LABKEY.vis.Scale.Shape()[0]}, - {text: 'QC', color: scope.colors['qc'], shape:LABKEY.vis.Scale.Shape()[0]}, - {text: 'Unknown', color: scope.colors['unknown'], shape:LABKEY.vis.Scale.Shape()[0]}, - {text: '', separator: true}, - {text: 'Excluded', color: scope.colors['standard'], shape: LABKEY.vis.Scale.Shape()[1]}, - {text: '', separator: true} - ]; - }, - - formatLegendValue: function (value, exp) { - if (value == null) - return 'NaN'; - var rounded = Math.round(value * 100000) / 100000; - // Use scientific notation if the number is large or very small - if (exp && (rounded > 10000 || rounded < -10000 || (rounded > -0.00001 && rounded < 0.00001))) - rounded.toExponential(4) - return rounded; - } -}); \ No newline at end of file + }; + + LABKEY.targetedms.CalibrationCurve = CalibrationCurve; +})(); diff --git a/webapp/TargetedMS/js/ParetoPlotPanel.js b/webapp/TargetedMS/js/ParetoPlotPanel.js index d4f8473b4..6509dbe6a 100644 --- a/webapp/TargetedMS/js/ParetoPlotPanel.js +++ b/webapp/TargetedMS/js/ParetoPlotPanel.js @@ -5,220 +5,427 @@ * Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0 * * Created by binalpatel on 7/9/15. + * + * Plain JS/HTML implementation (no ExtJS). Renders one set of Pareto plots per guide set + * into the element identified by config.plotDivId. */ +if (!LABKEY.targetedms) { + LABKEY.targetedms = {}; +} -Ext4.define('LABKEY.targetedms.ParetoPlotPanel', { +(function() { - extend: 'LABKEY.targetedms.BaseQCPlotPanel', + const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; + const DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; - initComponent : function() - { - this.callParent(); + function ParetoPlotPanel(config) { + this.plotDivId = config.plotDivId; + this.metricPropArr = []; + this.plotWidth = null; + this._maskEl = null; - Ext4.get(this.plotDivId).mask("Loading..."); + this.mask('Loading...'); + const me = this; LABKEY.targetedms.QCMetricConfigLoader.getMetrics(this.initPlot, this, function() { - Ext4.get(this.plotDivId).unmask(); - Ext4.get(this.plotDivId).update('Failed to load'); + me.unmask(); + const el = me.getPlotDiv(); + if (el) { + el.innerHTML = 'Failed to load'; + } }); - }, + } - initPlot : function(metrics) { - this.metricPropArr = metrics; + ParetoPlotPanel.prototype = { - LABKEY.Ajax.request({ - url: LABKEY.ActionURL.buildURL('targetedms', 'GetQCMetricOutliers.api'), - success: this.processResponse, - failure: LABKEY.Utils.getCallbackWrapper(this.failureHandler), - scope: this - }); - }, + getPlotDiv: function() { + return document.getElementById(this.plotDivId); + }, - processResponse: function(response) { - Ext4.get(this.plotDivId).unmask(); + mask: function(text) { + const el = this.getPlotDiv(); + if (!el) { + return; + } + this.unmask(); + const m = document.createElement('div'); + m.className = 'lk-pareto-loading'; + m.style.padding = '10px'; + m.textContent = text || 'Loading...'; + el.appendChild(m); + this._maskEl = m; + }, + + unmask: function() { + if (this._maskEl && this._maskEl.parentNode) { + this._maskEl.parentNode.removeChild(this._maskEl); + } + this._maskEl = null; + }, - var parsed = JSON.parse(response.responseText); + initPlot: function(metrics) { + this.metricPropArr = metrics; - if (!parsed.sampleFiles || Object.keys(parsed.sampleFiles).length === 0) { - Ext4.get(this.plotDivId).update('
No sample files loaded yet. Import some via Skyline, AutoQC, or the Data Pipeline tab here in Panorama.
'); - return; - } + LABKEY.Ajax.request({ + url: LABKEY.ActionURL.buildURL('targetedms', 'GetQCMetricOutliers.api'), + success: this.processResponse, + failure: LABKEY.Utils.getCallbackWrapper(this.failureHandler, this), + scope: this + }); + }, - var guideSets = parsed.guideSets; + processResponse: function(response) { + this.unmask(); - Ext4.each(guideSets, function(guideSet) { - guideSet.stats = { - CUSUMm: {count: 0, data: []}, - CUSUMv: {count: 0, data: []}, - mR: {count: 0, data: []}, - Value: {count: 0, data: []} - }; + const parsed = JSON.parse(response.responseText); + const el = this.getPlotDiv(); + + if (!parsed.sampleFiles || Object.keys(parsed.sampleFiles).length === 0) { + if (el) { + el.innerHTML = '
No sample files loaded yet. Import some via Skyline, AutoQC, or the Data Pipeline tab here in Panorama.
'; + } + return; + } - Ext4.iterate(guideSet.MetricCounts, function(metricName, data) { - this.addOutlierToCounts(guideSet, data, metricName,'CUSUMm', 'CUSUMm', true); - this.addOutlierToCounts(guideSet, data, metricName, 'CUSUMv', 'CUSUMv', true); - this.addOutlierToCounts(guideSet, data, metricName, 'mR', 'Moving Range', true); - this.addOutlierToCounts(guideSet, data, metricName, 'Value', 'Metric Value', true); - }, this); + const guideSets = parsed.guideSets; + const me = this; + + guideSets.forEach(function(guideSet) { + guideSet.stats = { + CUSUMm: {count: 0, data: []}, + CUSUMv: {count: 0, data: []}, + mR: {count: 0, data: []}, + Value: {count: 0, data: []} + }; + + Object.keys(guideSet.MetricCounts).forEach(function(metricName) { + const data = guideSet.MetricCounts[metricName]; + me.addOutlierToCounts(guideSet, data, metricName, 'CUSUMm', 'CUSUMm', true); + me.addOutlierToCounts(guideSet, data, metricName, 'CUSUMv', 'CUSUMv', true); + me.addOutlierToCounts(guideSet, data, metricName, 'mR', 'Moving Range', true); + me.addOutlierToCounts(guideSet, data, metricName, 'Value', 'Metric Value', true); + }); - Ext4.iterate(guideSet.stats, function(outlierType, data) { - var dataSet = data.data; + Object.keys(guideSet.stats).forEach(function(outlierType) { + const data = guideSet.stats[outlierType]; + const dataSet = data.data; - var totalCount = 0; - var maxOutliers = 0; + let totalCount = 0; + let maxOutliers = 0; - //find total count per guidesetID - for (var i = 0; i < dataSet.length; i++) { - totalCount += dataSet[i]['count']; + // find total count per guidesetID + for (let i = 0; i < dataSet.length; i++) { + totalCount += dataSet[i]['count']; - if(maxOutliers < dataSet[i]['count']) - { - maxOutliers = dataSet[i]['count']; + if (maxOutliers < dataSet[i]['count']) { + maxOutliers = dataSet[i]['count']; + } } - } - //sort by count in descending order - var sortedDataset = dataSet.sort(function(a, b) { - var order = b.count - a.count; - if (order !== 0) - return order; - return a.metricLabel.localeCompare(b.metricLabel); + // sort by count in descending order + const sortedDataset = dataSet.sort(function(a, b) { + const order = b.count - a.count; + if (order !== 0) { + return order; + } + return a.metricLabel.localeCompare(b.metricLabel); + }); + + // calculate cumulative percentage on sorted data + for (let j = 0; j < sortedDataset.length; j++) { + sortedDataset[j].percent = (j === 0 ? 0 : sortedDataset[j - 1].percent) + ((sortedDataset[j].count / totalCount) * 100); + } + data.maxOutliers = maxOutliers; }); + }); + + let guideSetCount = 1; + guideSets.forEach(function(guideSetData) { + const id = "paretoPlot-GuideSet-" + guideSetCount; + const dateFormat = LABKEY.extDefaultDateTimeFormat || 'Y-m-d H:i'; + const title = "Training Start: " + me.formatDate(new Date(guideSetData.TrainingStart), dateFormat) + + (guideSetData.ReferenceEnd ? " - Reference End: " + me.formatDate(new Date(guideSetData.ReferenceEnd), dateFormat) : " - Training End: " + me.formatDate(new Date(guideSetData.TrainingEnd), dateFormat)); + + const webpartTitleBase = "Guide Set " + guideSetCount + ' '; + const wp = 'pareto-plot-wp'; + const fileBase = "ParetoPlot-Guide Set " + guideSetCount; + + me.addEachParetoPlot(id, webpartTitleBase, "Metric Value", wp, title, fileBase, guideSetData.stats.Value.data, guideSetData.stats.Value.maxOutliers); + me.addEachParetoPlot(id + '_mR', webpartTitleBase, "Moving Range", wp, title, fileBase + '_mR', guideSetData.stats.mR.data, guideSetData.stats.mR.maxOutliers); + me.addEachParetoPlot(id + '_CUSUMm', webpartTitleBase, "Mean CUSUM", wp, title, fileBase + '_CUSUMm', guideSetData.stats.CUSUMm.data, guideSetData.stats.CUSUMm.maxOutliers); + me.addEachParetoPlot(id + '_CUSUMv', webpartTitleBase, "Variability CUSUM", wp, title, fileBase + '_CUSUMv', guideSetData.stats.CUSUMv.data, guideSetData.stats.CUSUMv.maxOutliers); + + guideSetCount++; + }); + }, + + addOutlierToCounts: function(guideSet, data, metricName, propertyName, plotTypeParamValue, isCusum) { + const count = data[propertyName]; + guideSet.stats[propertyName].count += count; + const newData = { + metricLabel: metricName, + count: count, + metricId: data.MetricId, + TrainingStart: guideSet.TrainingStart, + ReferenceEnd: guideSet.ReferenceEnd, + plotType: plotTypeParamValue + }; + if (isCusum) { + newData.CUSUMNegative = data[propertyName + 'N']; + newData.CUSUMPositive = data[propertyName + 'P']; + } - //calculate cumulative percentage on sorted data - for(var j = 0; j < sortedDataset.length; j++) { - sortedDataset[j].percent = (j == 0 ? 0 : sortedDataset[j-1].percent) + ((sortedDataset[j].count / totalCount) * 100); + guideSet.stats[propertyName].data.push(newData); + }, + + addEachParetoPlot: function(id, wpTitle, plotType, wp, plotTitle, fileName, plotData, yAxisMax) { + this.addPlotWebPartToPlotDiv(id, wpTitle, wp); + this.setPlotWidth(); + this.plotPareto(id, plotData, plotTitle, yAxisMax, plotType); + this.attachPlotExportIcons(id, id, 0, this.plotWidth - 30, 0); + }, + + plotPareto: function(id, data, title, yAxisMax, plotType) { + let tickValues; + if (yAxisMax < 10) { + tickValues = []; + for (let i = 0; i <= yAxisMax; i++) { + tickValues.push(i); } - data.maxOutliers = maxOutliers; - }, this) - }, this); - - - var guideSetCount = 1; - Ext4.each(guideSets, function(guideSetData) { - var id = "paretoPlot-GuideSet-"+guideSetCount; - const dateFormat = LABKEY.extDefaultDateTimeFormat || 'Y-m-d H:i'; - var title = "Training Start: " + Ext4.util.Format.date(new Date(guideSetData.TrainingStart), dateFormat) - + (guideSetData.ReferenceEnd ? " - Reference End: " + Ext4.util.Format.date(new Date(guideSetData.ReferenceEnd), dateFormat) : " - Training End: " + Ext4.util.Format.date(new Date(guideSetData.TrainingEnd), dateFormat)); - - var plotIdSuffix = '', plotType = "Metric Value", plotWp = 'pareto-plot-wp', plotData = guideSetData.stats.Value.data, plotMaxY = guideSetData.stats.Value.maxOutliers; - var webpartTitleBase = "Guide Set " + guideSetCount + ' '; - this.addEachParetoPlot(id, webpartTitleBase, plotType, plotWp, title, "ParetoPlot-Guide Set "+guideSetCount + plotIdSuffix, plotData, plotMaxY); - - plotIdSuffix = '_mR'; plotType = "Moving Range"; plotData = guideSetData.stats.mR.data; plotMaxY = guideSetData.stats.mR.maxOutliers; - this.addEachParetoPlot(id + plotIdSuffix, webpartTitleBase, plotType, plotWp, title, "ParetoPlot-Guide Set "+guideSetCount + plotIdSuffix, plotData, plotMaxY); - - plotIdSuffix = '_CUSUMm'; plotType = "Mean CUSUM"; plotData = guideSetData.stats.CUSUMm.data; plotMaxY = guideSetData.stats.CUSUMm.maxOutliers; - this.addEachParetoPlot(id + plotIdSuffix, webpartTitleBase, plotType, plotWp, title, "ParetoPlot-Guide Set "+guideSetCount + plotIdSuffix, plotData, plotMaxY); - - plotIdSuffix = '_CUSUMv'; plotType = "Variability CUSUM"; plotData = guideSetData.stats.CUSUMv.data; plotMaxY = guideSetData.stats.CUSUMv.maxOutliers; - this.addEachParetoPlot(id + plotIdSuffix, webpartTitleBase, plotType, plotWp, title, "ParetoPlot-Guide Set "+guideSetCount + plotIdSuffix, plotData, plotMaxY); - - guideSetCount++; - }, this); - }, - - addOutlierToCounts: function(guideSet, data, metricName, propertyName, plotTypeParamValue, isCusum) { - var count = data[propertyName]; - guideSet.stats[propertyName].count += count; - var newData = { - metricLabel: metricName, - count: count, - metricId: data.MetricId, - TrainingStart: guideSet.TrainingStart, - ReferenceEnd: guideSet.ReferenceEnd, - plotType: plotTypeParamValue - }; - if (isCusum) { - newData.CUSUMNegative = data[propertyName + 'N']; - newData.CUSUMPositive = data[propertyName + 'P']; - } - - guideSet.stats[propertyName].data.push(newData); - - }, - - addEachParetoPlot: function (id, wpTitle, plotType, wp, plotTitle, fileName, plotData, yAxisMax) - { - this.addPlotWebPartToPlotDiv(id, wpTitle, this.plotDivId, wp); - this.setPlotWidth(this.plotDivId); - this.plotPareto(id, plotData, plotTitle, yAxisMax, plotType); - this.attachPlotExportIcons(id, id, 0, this.plotWidth - 30, 0); - }, - - plotPareto: function(id, data, title, yAxisMax, plotType) - { - var tickValues; - if (yAxisMax < 10) { - tickValues = []; - for (var i = 0; i <= yAxisMax; i++) { - tickValues.push(i); } - } - var hoverFn = plotType.indexOf('CUSUM') > -1 ? this.plotBarHoverEvent : undefined; - var barChart = new LABKEY.vis.Plot({ - renderTo: id, - rendererType: 'd3', - width: this.plotWidth - 30, - height: 500, - data: Ext4.Array.clone(data), - labels: { - main: {value: "Pareto Plot - " + plotType}, - subtitle: {value: title, color: '#555555'}, - yLeft: {value: '# Outliers'}, - yRight: {value: 'Cumulative Percentage'} - }, - layers : [ - new LABKEY.vis.Layer({ - geom: new LABKEY.vis.Geom.BarPlot({clickFn: this.plotBarClickEvent, hoverFn: hoverFn}) - }), - new LABKEY.vis.Layer({ - geom: new LABKEY.vis.Geom.Path({color: 'steelblue'}), - aes: { x: 'metricLabel', yRight: 'percent' } - }), - new LABKEY.vis.Layer({ - geom: new LABKEY.vis.Geom.Point({color: 'steelblue'}), - aes: { x: 'metricLabel', yRight: 'percent', hoverText: function(val){return val.percent.toPrecision(4) + "%"}} - }) - ], - aes: { - x: 'metricLabel', - y: 'count' - }, - scales : { - x : { - scaleType: 'discrete', - tickHoverText: function(val) { - return val; - } + const hoverFn = plotType.indexOf('CUSUM') > -1 ? this.plotBarHoverEvent : undefined; + const barChart = new LABKEY.vis.Plot({ + renderTo: id, + rendererType: 'd3', + width: this.plotWidth - 30, + height: 500, + data: data.slice(), + labels: { + main: {value: "Pareto Plot - " + plotType}, + subtitle: {value: title, color: '#555555'}, + yLeft: {value: '# Outliers'}, + yRight: {value: 'Cumulative Percentage'} + }, + layers: [ + new LABKEY.vis.Layer({ + geom: new LABKEY.vis.Geom.BarPlot({clickFn: this.plotBarClickEvent, hoverFn: hoverFn}) + }), + new LABKEY.vis.Layer({ + geom: new LABKEY.vis.Geom.Path({color: 'steelblue'}), + aes: {x: 'metricLabel', yRight: 'percent'} + }), + new LABKEY.vis.Layer({ + geom: new LABKEY.vis.Geom.Point({color: 'steelblue'}), + aes: {x: 'metricLabel', yRight: 'percent', hoverText: function(val) {return val.percent.toPrecision(4) + "%"}} + }) + ], + aes: { + x: 'metricLabel', + y: 'count' }, - yLeft : { - domain: [0, (yAxisMax==0 ? 1 : yAxisMax)], - tickValues: tickValues + scales: { + x: { + scaleType: 'discrete', + tickHoverText: function(val) { + return val; + } + }, + yLeft: { + domain: [0, (yAxisMax === 0 ? 1 : yAxisMax)], + tickValues: tickValues + }, + yRight: { + domain: [0, 100] + } }, - yRight : { - domain: [0, 100] + margins: { + bottom: 75 } - }, - margins: { - bottom: 75 + }); + barChart.render(); + }, + + plotBarClickEvent: function(event, row) { + const params = {startDate: row.TrainingStart, metric: row.metricId, plotTypes: row.plotType}; + if (row.ReferenceEnd) { + params.endDate = row.ReferenceEnd; } - }); - barChart.render(); - }, - - plotBarClickEvent : function(event, row) { - var params = {startDate: row.TrainingStart, metric: row.metricId, plotTypes: row.plotType}; - if (row.ReferenceEnd) - { - params.endDate = row.ReferenceEnd; - } - window.location = LABKEY.ActionURL.buildURL('project', 'begin', null, params); - }, + window.location = LABKEY.ActionURL.buildURL('project', 'begin', null, params); + }, + + plotBarHoverEvent: function(row) { + const CUSUMN = row.CUSUMNegative ? row.CUSUMNegative : 0, CUSUMP = row.CUSUMPositive ? row.CUSUMPositive : 0; + return 'CUSUM-:' + ' ' + CUSUMN + '\nCUSUM+:' + ' ' + CUSUMP + '\nTotal: ' + row.count; + }, + + // ---- helpers previously inherited from the ExtJS BaseQCPlotPanel ---- + + getPlotWebPartHeader: function(wp, title) { + return '
' + + '' + + ' ' + + ' ' + + ' '; + }, + + addPlotWebPartToPlotDiv: function(id, title, wp) { + let html = this.getPlotWebPartHeader(wp, title); + html += '' + + ' ' + + ' ' + + '
' + + ' ' + LABKEY.Utils.encodeHtml(title) + '' + + '
' + + '
' + + '
'; + const div = this.getPlotDiv(); + if (div) { + div.insertAdjacentHTML('beforeend', html); + } + }, + + setPlotWidth: function() { + if (this.plotWidth == null) { + // set the width of the plot webparts based on the first labkey-wp-body element + this.plotWidth = 900; + const spacer = 33; + const wp = document.querySelector('.panel.panel-portal'); + if (wp && (wp.clientWidth - spacer) > this.plotWidth) { + this.plotWidth = wp.clientWidth - spacer; + } - plotBarHoverEvent : function(row) { - var CUSUMN = row.CUSUMNegative ? row.CUSUMNegative : 0, CUSUMP = row.CUSUMPositive ? row.CUSUMPositive : 0; - return 'CUSUM-:' + ' ' + CUSUMN + '\nCUSUM+:' + ' ' + CUSUMP + '\nTotal: ' + row.count; + const div = this.getPlotDiv(); + if (div) { + div.style.width = this.plotWidth + 'px'; + } + } + }, + + attachPlotExportIcons: function(id, plotTitle, plotIndex, plotWidth, extraMargin) { + const me = this; + this.createExportIcon(id, 'fa-file-pdf-o', 'Export to PDF', 0, plotIndex, plotWidth, function() { + me.exportChartToImage(id, extraMargin, LABKEY.vis.SVGConverter.FORMAT_PDF, plotTitle); + }); + + this.createExportIcon(id, 'fa-file-image-o', 'Export to PNG', 1, plotIndex, plotWidth, function() { + me.exportChartToImage(id, extraMargin, LABKEY.vis.SVGConverter.FORMAT_PNG, plotTitle); + }); + }, + + createExportIcon: function(divId, iconCls, tooltip, indexFromLeft, plotIndex, plotWidth, callbackFn) { + const leftPositionPx = (indexFromLeft * 30) + 60, + exportIconDivId = divId + iconCls, + html = '
' + + '
'; + + const container = document.getElementById(divId); + if (container) { + container.insertAdjacentHTML('afterbegin', html); + const iconEl = document.getElementById(exportIconDivId); + if (iconEl) { + iconEl.addEventListener('click', callbackFn); + } + } + }, + + exportChartToImage: function(svgDivId, extraMargin, type, fileName) { + const svgStr = this.getExportSVGStr(svgDivId, extraMargin), + exportType = type || LABKEY.vis.SVGConverter.FORMAT_PDF; + LABKEY.vis.SVGConverter.convert(svgStr, exportType, fileName); + }, + + getExportSVGStr: function(svgDivId, extraWidth) { + const container = document.getElementById(svgDivId); + const targetSvg = container.querySelector('svg'); + const oldWidth = targetSvg.getBoundingClientRect().width; + // temporarily increase svg size to allow exporting of legends that's outside svg + if (extraWidth) { + targetSvg.setAttribute('width', oldWidth + extraWidth); + } + let svgStr = LABKEY.vis.SVGConverter.svgToStr(targetSvg); + if (extraWidth) { + targetSvg.setAttribute('width', oldWidth); + } + svgStr = svgStr.replace(/visibility="hidden"/g, 'visibility="visible"'); + return svgStr; + }, + + failureHandler: function(response) { + const plotDiv = this.getPlotDiv(); + if (plotDiv) { + this.unmask(); + if (!response) { + plotDiv.innerHTML = "Failure loading data"; + } + else if (response.message) { + plotDiv.innerHTML = "" + LABKEY.Utils.encodeHtml(response.message) + ""; + } + else { + plotDiv.innerHTML = "Error: " + LABKEY.Utils.encodeHtml(response.exception) + ""; + } + } + }, + + /** + * Minimal replacement for Ext4.util.Format.date, supporting the PHP/Ext-style tokens used by + * LABKEY.extDefaultDateTimeFormat (default 'Y-m-d H:i'). + */ + formatDate: function(date, format) { + if (!date || isNaN(date.getTime())) { + return ''; + } + const pad = function(n) { return (n < 10 ? '0' : '') + n; }; + const year = date.getFullYear(); + const month = date.getMonth(); + const day = date.getDate(); + const dow = date.getDay(); + const hours = date.getHours(); + const minutes = date.getMinutes(); + const seconds = date.getSeconds(); + let h12 = hours % 12; + if (h12 === 0) { + h12 = 12; + } - } -}); \ No newline at end of file + const tokens = { + Y: '' + year, + y: ('' + year).slice(-2), + m: pad(month + 1), + n: '' + (month + 1), + F: MONTHS[month], + M: MONTHS[month].slice(0, 3), + d: pad(day), + j: '' + day, + l: DAYS[dow], + D: DAYS[dow].slice(0, 3), + N: '' + (dow === 0 ? 7 : dow), + w: '' + dow, + H: pad(hours), + G: '' + hours, + h: pad(h12), + g: '' + h12, + i: pad(minutes), + s: pad(seconds), + A: hours < 12 ? 'AM' : 'PM', + a: hours < 12 ? 'am' : 'pm' + }; + + let out = ''; + for (let i = 0; i < format.length; i++) { + const c = format[i]; + if (c === '\\' && i + 1 < format.length) { + out += format[++i]; + } + else if (Object.prototype.hasOwnProperty.call(tokens, c)) { + out += tokens[c]; + } + else { + out += c; + } + } + return out; + } + }; + + LABKEY.targetedms.ParetoPlotPanel = ParetoPlotPanel; +})();